Compare commits

...

10 Commits

Author SHA1 Message Date
43b84456b7 更新棋子和羁绊的详细信息展示,优化提示框样式和交互逻辑,增强移动设备上的用户体验,确保提示框在不同设备上均能良好显示和操作。 2025-04-02 16:25:42 +08:00
943991e025 优化移动设备上的提示框样式和交互逻辑,增强用户体验,确保在不同设备上均能良好显示和操作。 2025-04-02 15:53:07 +08:00
79bd57b79d 实现权重配置更新和详细信息获取API,优化错误处理,新增棋子和羁绊的详细信息展示功能,提升用户体验和代码可维护性。 2025-04-02 15:48:36 +08:00
ee7daf9ad1 优化激活状态更新逻辑,移除多余的调试日志,提升代码可读性和维护性。 2025-04-02 15:04:48 +08:00
10c990bd95 更新激活状态样式,确保在CSS和JavaScript中正确处理激活项的显示逻辑,优化代码可读性和维护性。 2025-04-02 15:03:09 +08:00
ff5e98d296 重构显示激活项的逻辑,提取为独立函数以提高代码可读性和可维护性,同时更新状态时根据复选框状态动态调整显示内容。 2025-04-02 14:52:46 +08:00
265dd6bfa3 问题优化 2025-04-02 14:44:42 +08:00
5d4c52fba4 实现权重配置管理功能,支持通过命令行参数指定配置文件路径,新增权重调整功能,优化评分系统和推荐引擎的初始化,更新Web界面以显示和调整权重设置。 2025-04-02 12:33:25 +08:00
0362a9c3ab 增加web模块 2025-04-02 11:20:50 +08:00
1053ea7697 使用外置配置文件,代码优化 2025-04-02 10:50:51 +08:00
22 changed files with 4109 additions and 615 deletions

251
README.md
View File

@ -27,6 +27,11 @@
- 交互式命令行界面,便于直接使用 - 交互式命令行界面,便于直接使用
- 支持自定义评分参数 - 支持自定义评分参数
5. **Web界面模块**
- 基于Flask的Web界面提供图形化操作体验
- 可视化显示阵容推荐结果
- 支持在线调整权重参数与配置
## 环境与依赖 ## 环境与依赖
- **Python版本:** Python 3.8+ - **Python版本:** Python 3.8+
@ -34,7 +39,12 @@
- requests: 用于获取在线数据 - requests: 用于获取在线数据
- pandas: 用于数据处理 - pandas: 用于数据处理
- pyyaml: 用于配置文件解析 - pyyaml: 用于配置文件解析
- numpy: 用于数值计算
- flask: 用于Web界面
- flask-cors: 处理跨域请求
- waitress: 生产环境Web服务器
- pytest: 用于单元测试 - pytest: 用于单元测试
- tqdm: 用于进度显示
## 项目安装 ## 项目安装
@ -51,6 +61,43 @@ pip install -r requirements.txt
## 快速开始 ## 快速开始
### 查看帮助信息
运行以下命令查看主程序的帮助信息:
```bash
python main.py --help
```
输出结果会显示所有可用的子命令和选项。
### Web界面
运行以下命令启动Web界面
```bash
python main.py web
```
默认情况下Web服务器将在`http://localhost:5000`上运行。您可以使用以下参数自定义服务器配置:
- `--host`: 指定服务器主机地址,默认为"0.0.0.0"(允许所有网络接口访问)
- `--port`: 指定服务器端口默认为5000
- `--dev`: 启用开发模式(开启调试功能,自动重载代码)
- `--config`: 指定自定义配置文件路径
例如:
```bash
python main.py web --host 127.0.0.1 --port 8080 --dev
```
Web界面提供以下功能
- 通过滑动条调整基础权重(羁绊等级权重、羁绊数量权重、棋子费用权重)
- 为每个具体的羁绊和棋子设置自定义权重
- 指定必选羁绊和必选棋子
- 一键生成最佳阵容并可视化显示
- 支持筛选显示已激活的羁绊和棋子
### 数据提供模块演示 ### 数据提供模块演示
运行以下命令启动数据提供模块的演示程序: 运行以下命令启动数据提供模块的演示程序:
@ -83,7 +130,7 @@ python main.py recommend
运行以下命令启动评分系统的演示程序: 运行以下命令启动评分系统的演示程序:
```bash ```bash
python src/scoring_demo.py python main.py scoring
``` ```
演示程序将展示以下功能: 演示程序将展示以下功能:
@ -93,6 +140,38 @@ python src/scoring_demo.py
- 比较不同权重设置下的评分差异 - 比较不同权重设置下的评分差异
- 动态修改配置文件并重新加载 - 动态修改配置文件并重新加载
您还可以指定自定义配置文件:
```bash
python main.py scoring --config path/to/custom_config.yaml
```
### 配置管理
运行以下命令管理权重配置:
```bash
python main.py config --show
```
配置管理提供以下功能:
- 显示当前所有权重配置
- 设置特定羁绊的权重
- 设置特定棋子的权重
- 设置基础权重参数
例如:
```bash
# 设置羁绊权重
python main.py config --set-synergy "超频战士" 1.5
# 设置棋子权重
python main.py config --set-chess "厄加特" 1.8
# 设置基础权重参数
python main.py config --set-base "synergy_level_weight" 1.2
```
### 命令行界面 ### 命令行界面
运行以下命令启动交互式命令行界面: 运行以下命令启动交互式命令行界面:
@ -112,6 +191,14 @@ python main.py cli
python main.py cli --population 8 --results 5 --level-weight 1.2 --count-weight 0.7 --cost-weight 0.2 python main.py cli --population 8 --results 5 --level-weight 1.2 --count-weight 0.7 --cost-weight 0.2
``` ```
参数说明:
- `--population`阵容人口数量范围1-10默认为9
- `--results`推荐结果数量默认为3
- `--level-weight`羁绊等级权重默认为1.0
- `--count-weight`羁绊数量权重默认为0.5
- `--cost-weight`棋子费用权重默认为0.1
- `--config`:指定自定义配置文件路径
## 项目结构 ## 项目结构
``` ```
@ -137,16 +224,34 @@ TFT-Strategist/
│ │ ├── __init__.py │ │ ├── __init__.py
│ │ ├── api.py # 编程接口 │ │ ├── api.py # 编程接口
│ │ └── cli.py # 命令行界面 │ │ └── cli.py # 命令行界面
│ ├── config/ # 配置管理模块
│ │ ├── __init__.py
│ │ └── weights_config.py # 权重配置管理
│ ├── web/ # Web界面模块
│ │ ├── __init__.py
│ │ ├── app.py # Flask应用
│ │ ├── static/ # 静态资源
│ │ │ ├── css/ # CSS样式
│ │ │ │ └── style.css
│ │ │ └── js/ # JavaScript脚本
│ │ │ └── main.js
│ │ └── templates/ # HTML模板
│ │ └── index.html
│ ├── __init__.py │ ├── __init__.py
│ ├── data_provider_demo.py # 数据提供模块演示 │ ├── data_provider_demo.py # 数据提供模块演示
│ ├── recommendation_demo.py # 阵容推荐模块演示 │ ├── recommendation_demo.py # 阵容推荐模块演示
│ └── scoring_demo.py # 评分系统演示 │ ├── scoring_demo.py # 评分系统演示
│ └── test_scoring.py # 评分测试模块
├── tests/ # 测试代码 ├── tests/ # 测试代码
│ ├── test_data_provider.py # 数据提供模块测试 │ ├── test_data_provider.py # 数据提供模块测试
│ ├── test_recommendation.py # 阵容推荐模块测试 │ ├── test_recommendation.py # 阵容推荐模块测试
│ └── test_interface.py # 接口模块测试 │ └── test_interface.py # 接口模块测试
├── main.py # 主程序入口 ├── main.py # 主程序入口
├── requirements.txt # 项目依赖 ├── requirements.txt # 项目依赖
├── test_direct.py # 直接测试脚本
├── test_recommendation.py # 阵容推荐测试脚本
├── generate_weights_config.py # 权重配置生成工具
├── get_data.py # 数据获取工具
└── README.md # 项目文档 └── README.md # 项目文档
``` ```
@ -250,75 +355,93 @@ for team in teams:
## 阵容评分模块详解 ## 阵容评分模块详解
阵容评分模块提供了对阵容进行综合评分的功能,考虑多个因素 阵容评分模块是系统的核心评估组件,负责为推荐的阵容提供量化的评分指标
### 主要组件 ### 主要组件
1. **TeamScorer**: 阵容评分系统 1. **TeamScorer**: 阵容评分
- 综合考虑羁绊数量、等级、棋子费用等因素 - 综合考虑羁绊数量、等级、棋子费用等因素
- 提供可配置的评分权重 - 支持自定义权重配置
- 多维度评估阵容强度 - 提供灵活的评分API
2. **ScoringConfig**: 评分配置类 2. **ScoringConfig**: 评分配置类
- 定义各项评分权重 - 定义评分所需的各项权重参数
- 支持自定义评分策略 - 提供默认配置值
- 支持通过外部配置文件进行定制
3. **ConfigLoader**: 配置加载器 3. **ConfigLoader**: 配置加载器
- 从外部YAML或JSON文件加载自定义权重设置 - 从YAML或JSON文件加载评分配置
- 支持修改和重新加载配置 - 支持热重载配置
- 提供丰富的配置访问API
### 评分因素 ### 配置文件详解
1. **羁绊等级评分**:基于羁绊等级和额外激活的羁绊单位数量 系统支持通过YAML配置文件data/weights_config.yaml来自定义评分权重
2. **羁绊种类数量评分**:基于激活的羁绊种类数量
3. **棋子费用评分**:基于阵容中棋子的费用分布
4. **自定义权重评分**:基于配置文件中设置的特定羁绊和棋子权重
### 自定义权重配置
你可以通过编辑 `data/weights_config.yaml` 文件来自定义评分权重:
```yaml ```yaml
# 基础权重配置 # 基础权重配置
base_weights: base_weights:
synergy_level_weight: 1.0 # 羁绊等级权重 synergy_level_weight: 1.0 # 羁绊等级权重
synergy_count_weight: 0.5 # 羁绊种类数量权重 synergy_count_weight: 0.5 # 羁绊数量权重
chess_cost_weight: 0.1 # 棋子费用权重 chess_cost_weight: 0.1 # 棋子费用权重
# 羁绊权重配置(值越大,该羁绊在评分中的权重越高) # 羁绊权重配置
synergy_weights: synergy_weights:
重装战士: 1.5 重装战士: 1.5 # 特定羁绊权重
魔法师: 1.2 魔法师: 1.2
# 可以添加更多羁绊... # 其他羁绊...
# 棋子权重配置(值越大,该棋子在评分中的权重越高) # 棋子权重配置
chess_weights: chess_weights:
亚索: 1.5 亚索: 1.5 # 特定棋子权重
艾希: 1.2 艾希: 1.2
# 可以添加更多棋子... # 其他棋子...
# 羁绊等级权重
synergy_level_weights:
'1': 1.0 # 1级羁绊权重
'2': 1.2 # 2级羁绊权重
# 其他等级...
# 棋子费用权重
cost_weights:
'1': 1.0 # 1费棋子权重
'2': 1.2 # 2费棋子权重
# 其他费用...
``` ```
配置文件中的权重设置将直接影响阵容评分结果。权重值越大,对应的因素在评分中的影响越大。
### 使用示例 ### 使用示例
```python ```python
from src.scoring import TeamScorer from src.scoring import TeamScorer
from src.data_provider import DataQueryAPI
from src.recommendation import RecommendationEngine
# 使用默认权重初始化评分器 # 初始化评分器(自动加载配置文件)
scorer = TeamScorer() scorer = TeamScorer()
# 从配置文件加载自定义权重 # 或者指定配置文件路径
scorer_with_config = TeamScorer(config_path="data/weights_config.yaml") scorer = TeamScorer(config_path="path/to/custom_config.yaml")
# 动态设置特定羁绊的权重 # 动态修改权重
scorer.set_synergy_weight("狙神", 2.0) scorer.set_synergy_weight("忍者", 2.0) # 提高"忍者"羁绊的权重
scorer.set_synergy_weight("刺客", 1.8) scorer.set_chess_weight("劫", 1.8) # 提高"劫"的权重
# 动态设置特定棋子的权重 # 重新加载配置文件
scorer.set_chess_weight("艾希", 2.0) scorer.reload_config()
scorer.set_chess_weight("薇恩", 1.8)
# 评分阵容 # 为阵容评分
score = scorer.score_team(team) api = DataQueryAPI()
engine = RecommendationEngine(api)
teams = engine.recommend_team(population=8)
for team in teams:
score = scorer.score_team(team)
print(f"阵容评分: {score}")
print(f"棋子: {[chess['displayName'] for chess in team.chess_list]}")
print(f"羁绊: {team.synergy_levels}")
``` ```
## 接口模块详解 ## 接口模块详解
@ -400,20 +523,68 @@ python main.py cli [参数]
2. 添加必选棋子 2. 添加必选棋子
3. 获取详细的阵容推荐结果 3. 获取详细的阵容推荐结果
## Web界面模块详解
Web界面模块提供了图形化的用户交互方式基于Flask框架实现。
### 主要功能
1. **阵容推荐设置**
- 可视化选择阵容人口
- 添加/删除必选羁绊和棋子
- 自定义权重调整
2. **阵容展示**
- 直观显示推荐阵容棋子和羁绊
- 支持多种排序和筛选方式
- 详细的阵容分析数据
3. **配置管理**
- 在线编辑权重配置
- 保存和加载配置文件
- 配置模板管理
### 启动方式
```bash
python main.py web [--host HOST] [--port PORT] [--dev] [--config CONFIG_PATH]
```
## 工具脚本
### generate_weights_config.py
用于生成初始权重配置文件,可根据游戏最新数据自动更新配置。
```bash
python generate_weights_config.py [--output OUTPUT_PATH]
```
### get_data.py
用于从官方接口获取最新游戏数据,支持手动更新。
```bash
python get_data.py [--force]
```
## 开发计划 ## 开发计划
- [x] 数据提供模块 - [x] 数据提供模块
- [x] 阵容推荐模块 - [x] 阵容推荐模块
- [x] 阵容评分模块 - [x] 阵容评分模块
- [x] 接口模块 - [x] 接口模块
- [x] Web界面模块
- [x] 配置管理功能
## 未来展望 ## 未来展望
- [ ] 图形用户界面(GUI)开发 - [ ] 优化算法效率,支持更复杂的推荐策略
- [ ] 基于历史数据的胜率分析 - [ ] 基于历史数据的胜率分析
- [ ] 支持装备推荐 - [ ] 支持装备推荐
- [ ] 多语言支持 - [ ] 多语言支持
- [ ] 基于机器学习的个性化推荐 - [ ] 基于机器学习的个性化推荐
- [ ] 移动端应用开发
## 贡献指南 ## 贡献指南

BIN
analysis_summary.txt Normal file

Binary file not shown.

View File

@ -1,121 +1,81 @@
# 云顶之弈阵容评分权重配置文件
# 基础权重配置
base_weights: base_weights:
synergy_level_weight: 1.0
synergy_count_weight: 0.5
chess_cost_weight: 0.1 chess_cost_weight: 0.1
synergy_count_weight: 0.5
# 羁绊权重配置(值越大,该羁绊在评分中的权重越高) synergy_level_weight: 1.0
synergy_weights: chess_weights:
# 职业羁绊 R-080T: 0
人造人: 1.4 T-43X: 0
召唤物: 0.7 T-URR37: 0
堡垒卫士: 1.4 乐芙兰: 1
强袭射手: 1.4 伊莉丝: 1
战略分析师: 1.1 佛耶戈: 1
斗士: 1.5 俄洛伊: 1
杀手: 1.4 克格莫: 1
裁决使: 1.4 加里奥: 1
超频战士: 1.6 : 1
迅捷射手: 1.5 千珏: 1
重装战士: 1.5 厄加特: 1
高级工程师: 1.5 厄斐琉斯: 1
厄运小姐: 1
# 特质羁绊 古拉加斯: 1
圣灵使者: 1.5 可酷伯: 1
幻灵战队: 1.4 吉格斯: 1
弑魂者: 1.6 嘉文四世: 1
战地机甲: 1.2 塞拉斯: 1
执事: 1.3 奈德丽: 1
源计划: 1.5 妮蔻: 1
病毒魔人: 1.0 婕拉: 1
福牛守护者: 1.2 安妮: 1
网络之神: 1.6 崔斯特: 1
街头恶魔: 1.4 布兰德: 1
赛博老大: 1.3 布隆: 1
辛迪加: 1.3 希瓦娜: 1
魔装机神: 1.1 德莱厄斯: 1
鳄霸: 1.6 德莱文: 1
悠米: 1
# 棋子费用等级权重(费用越高权重越大) 扎克: 1
拉亚斯特: 1
斯卡纳: 1
格雷福斯: 1
波比: 1
泽丽: 1
: 1
瑟庄妮: 1
盖伦: 1
科加斯: 1
纳亚菲利: 1
维迦: 1
艾克: 1
莎弥拉: 1
莫德凯撒: 1
莫甘娜: 1
萨勒芬妮: 1
萨科: 1
蒙多医生: 1
: 1
蕾欧娜: 1
薇古丝: 1
薇恩: 1
费德提克: 1
贾克斯: 1
赛娜: 1
金克丝: 1
阿利斯塔: 1
阿萝拉: 1
雷克顿: 1
雷恩加尔: 1
: 1
韦鲁斯: 1
魔像: 0
cost_weights: cost_weights:
'1': 1.0 '1': 1.0
'2': 1.2 '2': 1.2
'3': 1.5 '3': 1.5
'4': 1.8 '4': 1.8
'5': 2.0 '5': 2.0
# 棋子权重配置(值越大,该棋子在评分中的权重越高)
chess_weights:
R-080T: 0.8
T-43X: 0.8
T-URR37: 0.8
乐芙兰: 1.1
伊莉丝: 1.2
佛耶戈: 1.7
俄洛伊: 1.1
克格莫: 1.0
加里奥: 1.2
: 1.3
千珏: 1.0
厄加特: 1.4
厄斐琉斯: 1.3
厄运小姐: 1.3
古拉加斯: 1.2
可酷伯: 1.4
吉格斯: 1.3
嘉文四世: 1.2
塞拉斯: 1.0
奈德丽: 1.0
妮蔻: 1.3
婕拉: 1.0
安妮: 1.3
崔斯特: 1.1
布兰德: 1.5
布隆: 1.2
希瓦娜: 1.1
德莱厄斯: 1.1
德莱文: 1.2
悠米: 1.2
扎克: 1.7
拉亚斯特: 1.1
斯卡纳: 1.1
格雷福斯: 1.1
波比: 1.0
泽丽: 1.3
: 1.1
瑟庄妮: 1.3
盖伦: 1.7
科加斯: 1.3
纳亚菲利: 1.1
维迦: 1.1
艾克: 1.1
莎弥拉: 1.4
莫德凯撒: 1.2
莫甘娜: 1.0
萨勒芬妮: 1.0
萨科: 1.0
蒙多医生: 1.0
: 1.0
蕾欧娜: 1.3
薇古丝: 1.3
薇恩: 1.5
费德提克: 1.2
贾克斯: 1.0
赛娜: 1.5
金克丝: 1.5
阿利斯塔: 1.0
阿萝拉: 1.7
雷克顿: 1.4
雷恩加尔: 1.2
: 1.3
韦鲁斯: 1.2
魔像: 0.8
# 羁绊等级权重(不同等级的羁绊权重不同)
synergy_level_weights: synergy_level_weights:
'1': 1.0 '1': 1.0
'10': 3.5
'2': 1.2 '2': 1.2
'3': 1.5 '3': 1.5
'4': 1.8 '4': 1.8
@ -124,4 +84,30 @@ synergy_level_weights:
'7': 2.6 '7': 2.6
'8': 3.0 '8': 3.0
'9': 3.3 '9': 3.3
'10': 3.5 synergy_weights:
人造人: 1
召唤物: 0
圣灵使者: 1
堡垒卫士: 1
幻灵战队: 1
弑魂者: 1
强袭射手: 1
战地机甲: 1
战略分析师: 1
执事: 1
斗士: 1
杀手: 1
源计划: 1
病毒魔人: 1
福牛守护者: 1
网络之神: 1
街头恶魔: 1
裁决使: 1
赛博老大: 1
超频战士: 1
辛迪加: 1
迅捷射手: 1
重装战士: 1
高级工程师: 1
魔装机神: 1
鳄霸: 1

177
main.py
View File

@ -4,52 +4,165 @@
""" """
import sys import sys
import argparse import argparse
import logging
import os
from src.data_provider_demo import main as data_provider_demo from src.data_provider_demo import main as data_provider_demo
from src.recommendation_demo import main as recommendation_demo from src.recommendation_demo import main as recommendation_demo
from src.interface.cli import main as cli_main from src.interface.cli import main as cli_main
from src.scoring.scoring_system import TeamScorer
from src.test_scoring import main as test_scoring
from src.web import run_server # 导入Web模块
from src.config import get_global_weights_config
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("TFT-Strategist")
def main(): def main():
"""主函数""" """主程序入口"""
# 创建参数解析器 parser = argparse.ArgumentParser(description="云顶之弈阵容推荐器")
parser = argparse.ArgumentParser(description='云顶之弈阵容推荐器')
subparsers = parser.add_subparsers(dest='command', help='命令')
# 数据提供模块命令 # 添加子命令
data_parser = subparsers.add_parser('data', help='运行数据提供模块演示') subparsers = parser.add_subparsers(dest="command", help="可用命令")
# 阵容推荐模块命令 # 数据提供模块演示
recommend_parser = subparsers.add_parser('recommend', help='运行阵容推荐模块演示') data_parser = subparsers.add_parser("data", help="数据提供模块演示")
# 命令行界面命令 # 阵容推荐模块演示
cli_parser = subparsers.add_parser('cli', help='运行命令行界面') recommend_parser = subparsers.add_parser("recommend", help="阵容推荐模块演示")
recommend_parser.add_argument("--config", type=str, help="指定配置文件路径")
# 解析参数 # 命令行界面
cli_parser = subparsers.add_parser("cli", help="交互式命令行界面")
cli_parser.add_argument("--population", type=int, help="阵容人口数量")
cli_parser.add_argument("--results", type=int, help="推荐结果数量")
cli_parser.add_argument("--level-weight", type=float, help="羁绊等级权重")
cli_parser.add_argument("--count-weight", type=float, help="羁绊数量权重")
cli_parser.add_argument("--cost-weight", type=float, help="棋子费用权重")
cli_parser.add_argument("--config", type=str, help="指定配置文件路径")
# 评分模块测试
scoring_parser = subparsers.add_parser("scoring", help="评分模块测试")
scoring_parser.add_argument("--config", type=str, help="指定配置文件路径")
# 权重配置管理(新增)
config_parser = subparsers.add_parser("config", help="权重配置管理")
config_parser.add_argument("--show", action="store_true", help="显示当前权重配置")
config_parser.add_argument("--set-synergy", nargs=2, metavar=("NAME", "WEIGHT"), help="设置羁绊权重")
config_parser.add_argument("--set-chess", nargs=2, metavar=("NAME", "WEIGHT"), help="设置棋子权重")
config_parser.add_argument("--set-base", nargs=2, metavar=("PARAM", "WEIGHT"), help="设置基础权重参数")
config_parser.add_argument("--config", type=str, help="指定配置文件路径")
# Web界面
web_parser = subparsers.add_parser("web", help="启动Web界面")
web_parser.add_argument("--host", type=str, default="0.0.0.0", help="服务器主机地址")
web_parser.add_argument("--port", type=int, default=5000, help="服务器端口")
web_parser.add_argument("--dev", action="store_true", help="开发模式")
web_parser.add_argument("--config", type=str, help="指定配置文件路径")
# 解析命令行参数
args = parser.parse_args() args = parser.parse_args()
# 根据命令执行相应的功能 # 根据子命令执行相应功能
if args.command == 'data': if args.command == "data":
# 运行数据提供模块演示 logger.info("启动数据提供模块演示")
data_provider_demo() # 调用数据提供模块演示函数
elif args.command == 'recommend':
# 运行阵容推荐模块演示
recommendation_demo()
elif args.command == 'cli':
# 运行命令行界面
return cli_main()
else:
# 默认运行数据提供模块演示
print_usage(parser)
print("\n默认运行数据提供模块演示...\n")
data_provider_demo() data_provider_demo()
return 0 elif args.command == "recommend":
logger.info("启动阵容推荐模块演示")
# 调用阵容推荐模块演示函数
def print_usage(parser): recommendation_demo(config_path=getattr(args, 'config', None))
"""打印使用帮助"""
parser.print_help() elif args.command == "cli":
logger.info("启动交互式命令行界面")
# 将命令行参数传递给cli_main函数
return cli_main(
population=args.population,
results=args.results,
level_weight=args.level_weight,
count_weight=args.count_weight,
cost_weight=args.cost_weight,
config_path=getattr(args, 'config', None)
)
elif args.command == "scoring":
logger.info("启动评分模块测试")
test_scoring(config_path=getattr(args, 'config', None))
elif args.command == "config":
# 获取权重配置
config_path = getattr(args, 'config', None)
weights_config = get_global_weights_config(config_path)
if args.show:
# 显示当前配置
print("\n=== 当前权重配置 ===")
# 基础权重
print("\n基础权重:")
for key, value in weights_config.get_base_weights().items():
print(f" {key}: {value}")
# 羁绊权重
print("\n羁绊权重:")
for key, value in sorted(weights_config.get_synergy_weights().items()):
print(f" {key}: {value}")
# 棋子权重
print("\n棋子权重:")
for key, value in sorted(weights_config.get_chess_weights().items()):
print(f" {key}: {value}")
# 羁绊等级权重
print("\n羁绊等级权重:")
for key, value in sorted(weights_config.get_synergy_level_weights().items(),
key=lambda x: int(x[0])):
print(f" {key}: {value}")
# 棋子费用权重
print("\n棋子费用权重:")
for key, value in sorted(weights_config.get_cost_weights().items(),
key=lambda x: int(x[0])):
print(f" {key}: {value}")
elif args.set_synergy:
# 设置羁绊权重
name, weight = args.set_synergy
weights_config.set_synergy_weight(name, float(weight))
print(f"已设置羁绊 [{name}] 的权重为 {weight}")
elif args.set_chess:
# 设置棋子权重
name, weight = args.set_chess
weights_config.set_chess_weight(name, float(weight))
print(f"已设置棋子 [{name}] 的权重为 {weight}")
elif args.set_base:
# 设置基础权重参数
param, weight = args.set_base
weights_config.set_base_weight(param, float(weight))
print(f"已设置基础权重参数 [{param}] 的值为 {weight}")
else:
config_parser.print_help()
elif args.command == "web":
logger.info("启动Web界面")
# 调用Web服务器
run_server(
host=args.host,
port=args.port,
dev_mode=args.dev,
config_path=getattr(args, 'config', None)
)
else:
parser.print_help()
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main())

View File

@ -3,4 +3,7 @@ pandas>=1.2.4
pyyaml>=6.0 pyyaml>=6.0
pytest>=6.2.5 pytest>=6.2.5
numpy>=1.20.3 numpy>=1.20.3
tqdm>=4.62.3 tqdm>=4.62.3
flask>=2.0.1
flask-cors>=3.0.10
waitress>=2.1.2

11
src/config/__init__.py Normal file
View File

@ -0,0 +1,11 @@
"""
全局配置模块 - 提供整个应用的配置管理
此模块包含应用程序的全局配置管理功能包括
1. 权重配置的加载和管理
2. 全局参数的设置和获取
"""
from src.config.weights_config import WeightsConfig, get_global_weights_config
__all__ = ["WeightsConfig", "get_global_weights_config"]

View File

@ -0,0 +1,235 @@
"""
权重配置管理器 - 用于从外部配置文件加载并管理全局权重设置
此模块提供了从YAML或JSON配置文件中加载自定义权重设置的功能
支持为特定羁绊和棋子设置自定义权重并在整个应用程序中提供统一的权重配置
"""
import os
import json
import yaml
import logging
from typing import Dict, Any, Optional
# 配置日志
logger = logging.getLogger("TFT-Strategist-WeightsConfig")
# 全局权重配置实例
_global_weights_config = None
class WeightsConfig:
"""
权重配置管理器用于从外部文件加载自定义权重设置
"""
def __init__(self, config_path: Optional[str] = None):
"""
初始化权重配置管理器
Args:
config_path: 配置文件路径如果为None则使用默认路径
"""
self.config_path = config_path or os.path.join("data", "weights_config.yaml")
self.config_data = {}
# 自动加载配置文件
self.load_config()
def load_config(self) -> Dict[str, Any]:
"""
加载配置文件
Returns:
Dict[str, Any]: 包含配置数据的字典
"""
if not os.path.exists(self.config_path):
logger.warning(f"配置文件不存在: {self.config_path}")
# 如果文件不存在,创建默认配置
self._create_default_config()
return self.config_data
try:
file_ext = os.path.splitext(self.config_path)[1].lower()
if file_ext == '.json':
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config_data = json.load(f)
elif file_ext in ['.yaml', '.yml']:
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config_data = yaml.safe_load(f)
else:
logger.error(f"不支持的配置文件格式: {file_ext}")
self._create_default_config()
return self.config_data
logger.info(f"成功加载配置文件: {self.config_path}")
return self.config_data
except Exception as e:
logger.error(f"加载配置文件失败: {str(e)}")
# 如果加载失败,创建默认配置
self._create_default_config()
return self.config_data
def get_synergy_weights(self) -> Dict[str, float]:
"""
获取羁绊权重配置
Returns:
Dict[str, float]: 羁绊名称到权重的映射
"""
return self.config_data.get('synergy_weights', {})
def get_chess_weights(self) -> Dict[str, float]:
"""
获取棋子权重配置
Returns:
Dict[str, float]: 棋子名称到权重的映射
"""
return self.config_data.get('chess_weights', {})
def get_base_weights(self) -> Dict[str, float]:
"""
获取基础权重配置
Returns:
Dict[str, float]: 基础权重参数到权重值的映射
"""
return self.config_data.get('base_weights', {})
def get_synergy_level_weights(self) -> Dict[str, float]:
"""
获取羁绊等级权重配置
Returns:
Dict[str, float]: 羁绊等级到权重的映射
"""
return self.config_data.get('synergy_level_weights', {})
def get_cost_weights(self) -> Dict[str, float]:
"""
获取棋子费用权重配置
Returns:
Dict[str, float]: 棋子费用到权重的映射
"""
return self.config_data.get('cost_weights', {})
def set_synergy_weight(self, synergy_name: str, weight: float) -> None:
"""
设置特定羁绊的权重
Args:
synergy_name: 羁绊名称
weight: 权重值
"""
if 'synergy_weights' not in self.config_data:
self.config_data['synergy_weights'] = {}
self.config_data['synergy_weights'][synergy_name] = weight
self._save_config()
def set_chess_weight(self, chess_name: str, weight: float) -> None:
"""
设置特定棋子的权重
Args:
chess_name: 棋子名称
weight: 权重值
"""
if 'chess_weights' not in self.config_data:
self.config_data['chess_weights'] = {}
self.config_data['chess_weights'][chess_name] = weight
self._save_config()
def set_base_weight(self, param_name: str, weight: float) -> None:
"""
设置基础权重参数
Args:
param_name: 参数名称
weight: 权重值
"""
if 'base_weights' not in self.config_data:
self.config_data['base_weights'] = {}
self.config_data['base_weights'][param_name] = weight
self._save_config()
def _save_config(self) -> None:
"""保存配置到文件"""
try:
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
file_ext = os.path.splitext(self.config_path)[1].lower()
if file_ext == '.json':
with open(self.config_path, 'w', encoding='utf-8') as f:
json.dump(self.config_data, f, ensure_ascii=False, indent=2)
elif file_ext in ['.yaml', '.yml']:
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(self.config_data, f, allow_unicode=True, default_flow_style=False)
else:
logger.error(f"不支持的配置文件格式: {file_ext}")
return
logger.info(f"成功保存配置文件: {self.config_path}")
except Exception as e:
logger.error(f"保存配置文件失败: {str(e)}")
def _create_default_config(self) -> None:
"""创建默认配置文件"""
default_config = {
'base_weights': {
'synergy_level_weight': 1.0,
'synergy_count_weight': 0.5,
'chess_cost_weight': 0.1
},
'synergy_weights': {
'重装战士': 1.5,
'魔法师': 1.2,
'神谕者': 1.3,
'斗士': 1.1
},
'chess_weights': {
'亚索': 1.5,
'艾希': 1.2,
'璐璐': 1.3,
'金克斯': 1.4
},
'synergy_level_weights': {
'1': 1.0,
'2': 1.2,
'3': 1.5,
'4': 1.8,
'5': 2.0,
'6': 2.3,
'7': 2.6,
'8': 3.0,
'9': 3.3,
'10': 3.5
},
'cost_weights': {
'1': 1.0,
'2': 1.2,
'3': 1.5,
'4': 1.8,
'5': 2.0
}
}
self.config_data = default_config
self._save_config()
def get_global_weights_config(config_path: Optional[str] = None) -> WeightsConfig:
"""
获取全局权重配置实例
Args:
config_path: 配置文件路径如果为None则使用默认路径
Returns:
WeightsConfig: 全局权重配置实例
"""
global _global_weights_config
if _global_weights_config is None:
_global_weights_config = WeightsConfig(config_path)
return _global_weights_config

View File

@ -145,7 +145,9 @@ class DataQueryAPI:
Returns: Returns:
Optional[Dict[str, Any]]: 职业数据如果不存在则返回None Optional[Dict[str, Any]]: 职业数据如果不存在则返回None
""" """
return self._job_cache.get(job_id) # 确保job_id是字符串
job_id_str = str(job_id)
return self._job_cache.get(job_id_str)
def get_race_by_id(self, race_id: str) -> Optional[Dict[str, Any]]: def get_race_by_id(self, race_id: str) -> Optional[Dict[str, Any]]:
""" """
@ -157,7 +159,9 @@ class DataQueryAPI:
Returns: Returns:
Optional[Dict[str, Any]]: 特质数据如果不存在则返回None Optional[Dict[str, Any]]: 特质数据如果不存在则返回None
""" """
return self._race_cache.get(race_id) # 确保race_id是字符串
race_id_str = str(race_id)
return self._race_cache.get(race_id_str)
def get_chess_by_id(self, chess_id: str) -> Optional[Dict[str, Any]]: def get_chess_by_id(self, chess_id: str) -> Optional[Dict[str, Any]]:
""" """
@ -169,7 +173,9 @@ class DataQueryAPI:
Returns: Returns:
Optional[Dict[str, Any]]: 棋子数据如果不存在则返回None Optional[Dict[str, Any]]: 棋子数据如果不存在则返回None
""" """
return self._chess_cache.get(chess_id) # 确保chess_id是字符串
chess_id_str = str(chess_id)
return self._chess_cache.get(chess_id_str)
def get_job_by_name(self, name: str) -> Optional[Dict[str, Any]]: def get_job_by_name(self, name: str) -> Optional[Dict[str, Any]]:
""" """
@ -319,13 +325,16 @@ class DataQueryAPI:
Returns: Returns:
Optional[Dict[str, Any]]: 羁绊数据如果不存在则返回None Optional[Dict[str, Any]]: 羁绊数据如果不存在则返回None
""" """
# 确保synergy_id是字符串
synergy_id_str = str(synergy_id)
# 先在职业中查找 # 先在职业中查找
job = self.get_job_by_id(synergy_id) job = self.get_job_by_id(synergy_id_str)
if job: if job:
return job return job
# 再在特质中查找 # 再在特质中查找
return self.get_race_by_id(synergy_id) return self.get_race_by_id(synergy_id_str)
def get_synergy_by_name(self, name: str) -> Optional[Dict[str, Any]]: def get_synergy_by_name(self, name: str) -> Optional[Dict[str, Any]]:
""" """
@ -355,13 +364,16 @@ class DataQueryAPI:
Returns: Returns:
List[Dict[str, Any]]: 棋子数据列表 List[Dict[str, Any]]: 棋子数据列表
""" """
# 确保synergy_id是字符串
synergy_id_str = str(synergy_id)
# 先在职业中查找 # 先在职业中查找
chess_list = self.get_chess_by_job(synergy_id) chess_list = self.get_chess_by_job(synergy_id_str)
if chess_list: if chess_list:
return chess_list return chess_list
# 再在特质中查找 # 再在特质中查找
return self.get_chess_by_race(synergy_id) return self.get_chess_by_race(synergy_id_str)
def get_synergies_of_chess(self, chess_id: str) -> List[Dict[str, Any]]: def get_synergies_of_chess(self, chess_id: str) -> List[Dict[str, Any]]:
""" """
@ -418,7 +430,9 @@ class DataQueryAPI:
Returns: Returns:
Dict[str, str]: 羁绊等级信息键为等级值为效果描述 Dict[str, str]: 羁绊等级信息键为等级值为效果描述
""" """
synergy = self.get_synergy_by_id(synergy_id) # 确保synergy_id是字符串
synergy_id_str = str(synergy_id)
synergy = self.get_synergy_by_id(synergy_id_str)
if not synergy: if not synergy:
return {} return {}

View File

@ -16,6 +16,7 @@ import logging
from src.data_provider import DataQueryAPI from src.data_provider import DataQueryAPI
from src.recommendation import RecommendationEngine from src.recommendation import RecommendationEngine
from src.scoring import TeamScorer, ScoringConfig from src.scoring import TeamScorer, ScoringConfig
from src.config import get_global_weights_config
# 配置日志 # 配置日志
logging.basicConfig( logging.basicConfig(
@ -28,14 +29,32 @@ logger = logging.getLogger("TFT-Strategist-CLI")
class TFTCommandLine: class TFTCommandLine:
"""云顶之弈阵容推荐命令行接口""" """云顶之弈阵容推荐命令行接口"""
def __init__(self): def __init__(self, config_path: Optional[str] = None):
"""初始化命令行接口""" """
初始化命令行接口
Args:
config_path: 配置文件路径用于全局权重配置
"""
# 加载全局权重配置
self.weights_config = get_global_weights_config(config_path)
# 初始化API和推荐引擎
self.api = DataQueryAPI() self.api = DataQueryAPI()
self.recommendation_engine = RecommendationEngine(api=self.api) self.scorer = TeamScorer(config_path=config_path)
self.scorer = TeamScorer() self.recommendation_engine = RecommendationEngine(api=self.api, scorer=self.scorer, config_path=config_path)
def run(self): def run(self, population=None, results=None, level_weight=None, count_weight=None, cost_weight=None):
"""运行命令行界面""" """
运行命令行界面
Args:
population (int, optional): 阵容人口数量
results (int, optional): 推荐结果数量
level_weight (float, optional): 羁绊等级权重
count_weight (float, optional): 羁绊数量权重
cost_weight (float, optional): 棋子费用权重
"""
parser = argparse.ArgumentParser(description='云顶之弈阵容推荐器命令行界面', add_help=False) parser = argparse.ArgumentParser(description='云顶之弈阵容推荐器命令行界面', add_help=False)
parser.add_argument('--population', type=int, default=9, help='阵容人口数量默认为9') parser.add_argument('--population', type=int, default=9, help='阵容人口数量默认为9')
parser.add_argument('--results', type=int, default=3, help='返回结果数量默认为3') parser.add_argument('--results', type=int, default=3, help='返回结果数量默认为3')
@ -46,16 +65,25 @@ class TFTCommandLine:
# 解析参数,但忽略未知的参数,以便与主程序的参数解析兼容 # 解析参数,但忽略未知的参数,以便与主程序的参数解析兼容
args, _ = parser.parse_known_args() args, _ = parser.parse_known_args()
# 自定义评分权重 # 优先使用传入的参数,其次使用命令行参数
self.scorer.customize_scoring( population = population or args.population
synergy_level_weight=args.level_weight, max_results = results or args.results
synergy_count_weight=args.count_weight, level_weight = level_weight or args.level_weight
chess_cost_weight=args.cost_weight count_weight = count_weight or args.count_weight
) cost_weight = cost_weight or args.cost_weight
# 自定义评分权重,同时更新全局配置
if level_weight is not None:
self.weights_config.set_base_weight('synergy_level_weight', level_weight)
if count_weight is not None:
self.weights_config.set_base_weight('synergy_count_weight', count_weight)
if cost_weight is not None:
self.weights_config.set_base_weight('chess_cost_weight', cost_weight)
# 更新评分系统的配置
self.scorer.reload_config()
# 初始化阵容参数 # 初始化阵容参数
population = args.population
max_results = args.results
required_synergies = [] required_synergies = []
required_chess = [] required_chess = []
@ -64,6 +92,13 @@ class TFTCommandLine:
print(f"当前游戏版本: {version}") print(f"当前游戏版本: {version}")
print(f"阵容人口: {population}") print(f"阵容人口: {population}")
# 显示当前权重配置
base_weights = self.weights_config.get_base_weights()
print("\n当前评分权重配置:")
print(f" 羁绊等级权重: {base_weights.get('synergy_level_weight', 1.0)}")
print(f" 羁绊数量权重: {base_weights.get('synergy_count_weight', 0.5)}")
print(f" 棋子费用权重: {base_weights.get('chess_cost_weight', 0.1)}")
# 交互式添加必选羁绊 # 交互式添加必选羁绊
print("\n-- 添加必选羁绊 --") print("\n-- 添加必选羁绊 --")
while True: while True:
@ -99,6 +134,22 @@ class TFTCommandLine:
except ValueError: except ValueError:
print("请输入数字") print("请输入数字")
# 显示羁绊在配置中的权重
synergy_weight = self.weights_config.get_synergy_weights().get(synergy_name, 1.0)
print(f"羁绊 {synergy_name} 的当前权重为: {synergy_weight}")
# 可选:调整羁绊权重
adjust = input(f"是否调整该羁绊的权重?(y/n当前: {synergy_weight}): ").strip().lower()
if adjust == 'y':
while True:
try:
new_weight = float(input(f"请输入新权重值 (当前: {synergy_weight}): ").strip())
self.weights_config.set_synergy_weight(synergy_name, new_weight)
print(f"已将羁绊 {synergy_name} 的权重设置为 {new_weight}")
break
except ValueError:
print("请输入有效的数字")
required_synergies.append({ required_synergies.append({
'name': synergy_name, 'name': synergy_name,
'level': level 'level': level
@ -116,6 +167,22 @@ class TFTCommandLine:
if not chess: if not chess:
print(f"未找到棋子: {chess_name}") print(f"未找到棋子: {chess_name}")
continue continue
# 显示棋子在配置中的权重
chess_weight = self.weights_config.get_chess_weights().get(chess_name, 1.0)
print(f"棋子 {chess_name} 的当前权重为: {chess_weight}")
# 可选:调整棋子权重
adjust = input(f"是否调整该棋子的权重?(y/n当前: {chess_weight}): ").strip().lower()
if adjust == 'y':
while True:
try:
new_weight = float(input(f"请输入新权重值 (当前: {chess_weight}): ").strip())
self.weights_config.set_chess_weight(chess_name, new_weight)
print(f"已将棋子 {chess_name} 的权重设置为 {new_weight}")
break
except ValueError:
print("请输入有效的数字")
required_chess.append({ required_chess.append({
'name': chess_name 'name': chess_name
@ -178,18 +245,28 @@ class TFTCommandLine:
print("\n------------------------") print("\n------------------------")
def main(): def main(population=None, results=None, level_weight=None, count_weight=None, cost_weight=None, config_path=None):
"""主函数""" """
cli = TFTCommandLine() 主函数
Args:
population (int, optional): 阵容人口数量
results (int, optional): 推荐结果数量
level_weight (float, optional): 羁绊等级权重
count_weight (float, optional): 羁绊数量权重
cost_weight (float, optional): 棋子费用权重
config_path (str, optional): 配置文件路径
"""
try: try:
cli.run() cli = TFTCommandLine(config_path=config_path)
cli.run(population, results, level_weight, count_weight, cost_weight)
return 0
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n程序已退出") print("\n操作被用户中断")
except Exception as e: return 1
logger.error(f"发生错误: {e}", exc_info=True) except Exception as e:
print(f"\n程序发生错误: {e}") logger.error(f"发生错误: {str(e)}")
return 1 return 1
return 0
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -12,6 +12,7 @@ import itertools
from dataclasses import dataclass, field from dataclasses import dataclass, field
from src.data_provider import DataQueryAPI from src.data_provider import DataQueryAPI
from src.scoring.scoring_system import TeamScorer from src.scoring.scoring_system import TeamScorer
from src.config import get_global_weights_config
# 配置日志 # 配置日志
logging.basicConfig( logging.basicConfig(
@ -60,19 +61,22 @@ class TeamComposition:
# 确定各羁绊激活的等级 # 确定各羁绊激活的等级
for synergy_id, count in self.synergy_counts.items(): for synergy_id, count in self.synergy_counts.items():
synergy = api.get_synergy_by_id(synergy_id) # 确保synergy_id是字符串
synergy_id_str = str(synergy_id)
synergy = api.get_synergy_by_id(synergy_id_str)
if not synergy: if not synergy:
logger.warning(f"未找到羁绊: {{'id': {synergy_id}}}")
continue continue
# 确定激活的等级 # 确定激活的等级
levels = api.get_synergy_levels(synergy_id) levels = api.get_synergy_levels(synergy_id_str)
active_levels = [] active_levels = []
for level_str, effect in levels.items(): for level_str, effect in levels.items():
level = int(level_str) level = int(level_str)
if count >= level: if count >= level:
active_level = { active_level = {
'id': synergy_id, 'id': synergy_id_str,
'name': synergy.get('name', ''), 'name': synergy.get('name', ''),
'level': level, 'level': level,
'count': count, 'count': count,
@ -106,16 +110,26 @@ class RecommendationEngine:
阵容推荐引擎负责根据用户需求生成最优阵容 阵容推荐引擎负责根据用户需求生成最优阵容
""" """
def __init__(self, api: Optional[DataQueryAPI] = None, scorer: Optional[TeamScorer] = None): def __init__(self, api: DataQueryAPI, scorer: TeamScorer, config_path: Optional[str] = None, config_obj: Optional[Dict[str, Any]] = None):
""" """
初始化阵容推荐引擎 初始化阵容推荐引擎
Args: Args:
api: 数据查询API实例如果为None则创建一个新的实例 api: 数据查询API实例如果为None则创建一个新的实例
scorer: 阵容评分系统实例如果为None则创建一个新的实例 scorer: 阵容评分系统实例如果为None则创建一个新的实例
config_path: 配置文件路径用于全局权重配置
config_obj: 配置对象优先于config_path使用
""" """
self.api = api if api else DataQueryAPI() self.api = api if api else DataQueryAPI()
self.scorer = scorer if scorer else TeamScorer()
# 加载全局权重配置
if config_obj:
self.weights_config = config_obj
else:
self.weights_config = get_global_weights_config(config_path).config_data
# 创建阵容评分系统
self.scorer = scorer if scorer else TeamScorer(config_path=config_path)
def recommend_team( def recommend_team(
self, self,
@ -149,7 +163,7 @@ class RecommendationEngine:
if isinstance(chess_info, dict) and 'name' in chess_info: if isinstance(chess_info, dict) and 'name' in chess_info:
chess = self.api.get_chess_by_name(chess_info['name']) chess = self.api.get_chess_by_name(chess_info['name'])
elif isinstance(chess_info, dict) and 'id' in chess_info: elif isinstance(chess_info, dict) and 'id' in chess_info:
chess = self.api.get_chess_by_id(chess_info['id']) chess = self.api.get_chess_by_id(str(chess_info['id']))
else: else:
chess = self.api.get_chess_by_name(str(chess_info)) chess = self.api.get_chess_by_name(str(chess_info))
@ -172,7 +186,7 @@ class RecommendationEngine:
synergy = self.api.get_synergy_by_name(synergy_info['name']) synergy = self.api.get_synergy_by_name(synergy_info['name'])
min_level = synergy_info.get('level', 1) min_level = synergy_info.get('level', 1)
elif isinstance(synergy_info, dict) and 'id' in synergy_info: elif isinstance(synergy_info, dict) and 'id' in synergy_info:
synergy = self.api.get_synergy_by_id(synergy_info['id']) synergy = self.api.get_synergy_by_id(str(synergy_info['id']))
min_level = synergy_info.get('level', 1) min_level = synergy_info.get('level', 1)
else: else:
synergy = self.api.get_synergy_by_name(str(synergy_info)) synergy = self.api.get_synergy_by_name(str(synergy_info))
@ -195,27 +209,37 @@ class RecommendationEngine:
break break
if target_level == 0: if target_level == 0:
target_level = min_level logger.warning(f"羁绊 {synergy.get('name', '')} 找不到满足最低等级 {min_level} 的激活条件")
continue
# 考虑羁绊在全局配置中的权重
weight = self.weights_config.get('synergy_weights', {}).get(synergy.get('name', ''), 1.0)
synergy_chess_sets.append({ synergy_chess_sets.append({
'synergy': synergy, 'synergy': synergy,
'chess_list': chess_list, 'chess_list': chess_list,
'target_level': target_level 'target_level': target_level,
'weight': weight # 添加权重信息
}) })
# 3. 生成候选阵容 # 3. 根据已选棋子和羁绊要求生成候选阵容
candidate_teams = self._generate_candidate_teams( candidate_teams = self._generate_candidate_teams(base_team, synergy_chess_sets, population)
base_team, synergy_chess_sets, population
)
# 4. 评分排序并返回结果 # 如果没有生成候选阵容,则尝试填充最佳棋子
if not candidate_teams:
remaining_slots = population - base_team.size
logger.info(f"未找到满足所有羁绊要求的阵容,尝试填充最佳棋子 (剩余槽位: {remaining_slots})")
candidate_teams = self._fill_team_with_best_chess(base_team, remaining_slots)
# 4. 计算每个阵容的评分并排序
for team in candidate_teams: for team in candidate_teams:
team.calculate_synergies(self.api) team.calculate_synergies(self.api)
team.score = self.scorer.score_team(team) team.score = self.scorer.score_team(team)
# 按评分从高到低排序 # 按从高到低排序
candidate_teams.sort(key=lambda x: x.score, reverse=True) candidate_teams.sort(key=lambda t: t.score, reverse=True)
# 返回分数最高的几个阵容
return candidate_teams[:max_results] return candidate_teams[:max_results]
def _generate_candidate_teams( def _generate_candidate_teams(
@ -225,107 +249,105 @@ class RecommendationEngine:
population: int population: int
) -> List[TeamComposition]: ) -> List[TeamComposition]:
""" """
生成候选阵容 根据羁绊要求生成候选阵容
Args: Args:
base_team: 基础阵容包含必选棋子 base_team: 基础阵容包含必选棋子
synergy_chess_sets: 各个必选羁绊的棋子集合 synergy_chess_sets: 羁绊需要的棋子集合列表
population: 人口限制 population: 人口限制
Returns: Returns:
List[TeamComposition]: 候选阵容列表 List[TeamComposition]: 候选阵容列表
""" """
# 已经有基础棋子 # 如果没有羁绊要求,则直接填充最佳棋子
remaining_slots = population - base_team.size
# 如果没有必选羁绊,则直接从所有棋子中选择
if not synergy_chess_sets: if not synergy_chess_sets:
remaining_slots = population - base_team.size
return self._fill_team_with_best_chess(base_team, remaining_slots) return self._fill_team_with_best_chess(base_team, remaining_slots)
# 处理必选羁绊 # 排序羁绊集合,根据权重值从高到低排序
synergy_chess_sets.sort(key=lambda s: s.get('weight', 1.0), reverse=True)
# 开始构建组合
candidate_teams = [] candidate_teams = []
# 为每个羁绊选择合适的棋子 # 记录已经在基础阵容中的棋子
synergy_combinations = [] base_chess_names = {chess.get('displayName') for chess in base_team.chess_list}
# 1. 从每个羁绊中选择需要的棋子数量
# 创建每个羁绊至少需要的棋子数组合
min_chess_options = []
for synergy_set in synergy_chess_sets: for synergy_set in synergy_chess_sets:
synergy = synergy_set['synergy']
chess_list = synergy_set['chess_list'] chess_list = synergy_set['chess_list']
target_level = synergy_set['target_level'] target_level = synergy_set['target_level']
# 过滤掉已经在基础阵容中的棋子 # 计算需要添加的棋子数量
available_chess = [ already_in_base = sum(1 for chess in base_team.chess_list
chess for chess in chess_list if any(job_id in chess.get('jobIds', '').split(',')
if chess not in base_team.chess_list for job_id in [synergy_set['synergy'].get('jobId')])
] or any(race_id in chess.get('raceIds', '').split(',')
for race_id in [synergy_set['synergy'].get('raceId')]))
# 计算还需要多少个该羁绊的棋子 needed_count = max(0, target_level - already_in_base)
synergy_id = synergy.get('jobId') or synergy.get('raceId') if needed_count <= 0:
# 如果基础阵容已经满足该羁绊要求,则跳过
# 统计基础阵容中有多少个该羁绊的棋子
base_count = 0
for chess in base_team.chess_list:
job_ids = chess.get('jobIds', '').split(',')
race_ids = chess.get('raceIds', '').split(',')
if synergy_id in job_ids or synergy_id in race_ids:
base_count += 1
# 还需要的棋子数量
needed_count = max(0, target_level - base_count)
# 如果该羁绊已经满足要求,跳过
if needed_count == 0:
continue continue
# 从该羁绊的棋子中选择还未在基础阵容中的
available_chess = [chess for chess in chess_list
if chess.get('displayName') not in base_chess_names]
# 如果可用棋子不足,则尽可能多选 # 如果可用棋子不足以满足羁绊要求,则返回空列表
if len(available_chess) < needed_count: if len(available_chess) < needed_count:
needed_count = len(available_chess) logger.warning(f"羁绊 {synergy_set['synergy'].get('name', '')} 可用棋子不足,无法满足等级 {target_level} 的要求")
return []
# 生成该羁绊的所有可能组合 # 所有可能的棋子组合
combinations = list(itertools.combinations(available_chess, needed_count)) chess_combinations = list(itertools.combinations(available_chess, needed_count))
synergy_combinations.append(combinations)
# 将权重因素纳入棋子选择决策
weighted_combinations = []
for combo in chess_combinations:
# 计算组合的权重分数
combo_weight = sum(self.weights_config.get('chess_weights', {}).get(chess.get('displayName', ''), 1.0) for chess in combo)
weighted_combinations.append((combo, combo_weight))
# 按权重从高到低排序
weighted_combinations.sort(key=lambda x: x[1], reverse=True)
# 选择权重较高的前几个组合
top_combinations = [combo for combo, _ in weighted_combinations[:min(5, len(weighted_combinations))]]
min_chess_options.append(top_combinations)
# 如果没有有效的羁绊组合,则直接填充最好的棋子 # 如果某个羁绊无法满足,则返回空列表
if not synergy_combinations: if not all(min_chess_options):
return self._fill_team_with_best_chess(base_team, remaining_slots) return []
# 生成所有羁绊组合的笛卡尔积 # 2. 尝试组合不同羁绊的棋子,生成可行阵容
for combo_product in itertools.product(*synergy_combinations): # 为每个羁绊选择一种棋子组合方式
# 扁平化组合成一个列表 for chess_selection in itertools.product(*min_chess_options):
flat_combo = [] # 创建新的候选阵容
for combo in combo_product: candidate = TeamComposition()
flat_combo.extend(combo) # 添加基础阵容的棋子
# 去重
unique_combo = []
for chess in flat_combo:
if chess not in unique_combo:
unique_combo.append(chess)
# 如果组合后的棋子数量超过剩余槽位,跳过
if len(unique_combo) > remaining_slots:
continue
# 创建新的阵容
new_team = TeamComposition()
for chess in base_team.chess_list: for chess in base_team.chess_list:
new_team.add_chess(chess) candidate.add_chess(chess)
# 添加羁绊棋子 # 添加各羁绊选中的棋子
for chess in unique_combo: all_selected_chess = set()
new_team.add_chess(chess) for combo in chess_selection:
for chess in combo:
if chess.get('displayName') not in all_selected_chess:
candidate.add_chess(chess)
all_selected_chess.add(chess.get('displayName'))
# 如果还有剩余槽位,用最佳棋子填充 # 检查是否超出人口限制
if new_team.size < population: if candidate.size <= population:
new_remaining_slots = population - new_team.size # 如果有剩余人口,填充最佳棋子
filled_teams = self._fill_team_with_best_chess(new_team, new_remaining_slots) if candidate.size < population:
if filled_teams: remaining_slots = population - candidate.size
candidate_teams.extend(filled_teams) filled_candidates = self._fill_team_with_best_chess(candidate, remaining_slots)
else: candidate_teams.extend(filled_candidates)
candidate_teams.append(new_team) else:
candidate_teams.append(candidate)
# 如果没有生成候选阵容,直接填充最好的棋子
if not candidate_teams:
return self._fill_team_with_best_chess(base_team, remaining_slots)
return candidate_teams return candidate_teams
@ -335,7 +357,7 @@ class RecommendationEngine:
remaining_slots: int remaining_slots: int
) -> List[TeamComposition]: ) -> List[TeamComposition]:
""" """
用最佳棋子填充阵容剩余槽位 用最佳棋子填充剩余槽位
Args: Args:
base_team: 基础阵容 base_team: 基础阵容
@ -346,51 +368,94 @@ class RecommendationEngine:
""" """
if remaining_slots <= 0: if remaining_slots <= 0:
return [base_team] return [base_team]
# 获取所有可用棋子
all_chess = self.api.get_all_chess()
# 过滤掉已经在基础阵容中的棋子
available_chess = [
chess for chess in all_chess
if chess not in base_team.chess_list
]
# 按照一定策略选择最佳棋子
# 这里可以实现多种策略,如优先选择与现有阵容羁绊匹配的棋子、高费棋子等
# 简单起见,我们用一个贪心策略:计算每个棋子能为阵容增加的羁绊收益
# 先计算基础阵容的羁绊情况
base_team_copy = TeamComposition()
for chess in base_team.chess_list:
base_team_copy.add_chess(chess)
base_team_copy.calculate_synergies(self.api)
# 评估每个棋子的收益
chess_benefits = []
for chess in available_chess:
# 创建临时阵容
temp_team = TeamComposition()
for c in base_team.chess_list:
temp_team.add_chess(c)
temp_team.add_chess(chess)
temp_team.calculate_synergies(self.api)
# 计算羁绊增益 # 计算每个棋子的羁绊价值
benefit = self.scorer.score_team(temp_team) - self.scorer.score_team(base_team_copy) all_chess = self.api.get_all_chess()
chess_benefits.append((chess, benefit)) base_chess_names = {chess.get('displayName') for chess in base_team.chess_list}
# 按收益排序 # 排除已在基础阵容中的棋子
chess_benefits.sort(key=lambda x: x[1], reverse=True) available_chess = [chess for chess in all_chess
if chess.get('displayName') not in base_chess_names]
# 选择前N个最佳棋子 # 计算基础阵容中已有的羁绊
best_chess = [item[0] for item in chess_benefits[:remaining_slots]] base_synergies = {}
# 创建填充后的阵容
filled_team = TeamComposition()
for chess in base_team.chess_list: for chess in base_team.chess_list:
filled_team.add_chess(chess) # 职业
for chess in best_chess: for job_id in chess.get('jobIds', '').split(','):
filled_team.add_chess(chess) if job_id:
base_synergies[job_id] = base_synergies.get(job_id, 0) + 1
# 特质
for race_id in chess.get('raceIds', '').split(','):
if race_id:
base_synergies[race_id] = base_synergies.get(race_id, 0) + 1
return [filled_team] # 评估每个可用棋子的价值
chess_values = []
for chess in available_chess:
value = 0
# 根据费用评估基础价值
cost = int(chess.get('price', 1))
cost_multiplier = float(self.weights_config.get('cost_weights', {}).get(str(cost), 1.0))
value += cost * cost_multiplier
# 根据羁绊评估额外价值
for job_id in chess.get('jobIds', '').split(','):
if job_id and job_id in base_synergies:
# 如果能与基础阵容形成羁绊,增加价值
synergy = self.api.get_synergy_by_id(job_id)
if synergy:
# 检查是否能激活新的羁绊等级
levels = self.api.get_synergy_levels(job_id)
current_count = base_synergies[job_id]
for level_str in sorted(levels.keys()):
level = int(level_str)
if current_count + 1 >= level > current_count:
# 如果添加这个棋子后能激活新的等级
weight = self.weights_config.get('synergy_weights', {}).get(synergy.get('name', ''), 1.0)
level_multiplier = float(self.weights_config.get('synergy_level_weights', {}).get(level_str, 1.0))
value += weight * level_multiplier * 10
break
# 即使不能激活新等级,也增加一些价值
value += 1
for race_id in chess.get('raceIds', '').split(','):
if race_id and race_id in base_synergies:
# 如果能与基础阵容形成羁绊,增加价值
synergy = self.api.get_synergy_by_id(race_id)
if synergy:
# 检查是否能激活新的羁绊等级
levels = self.api.get_synergy_levels(race_id)
current_count = base_synergies[race_id]
for level_str in sorted(levels.keys()):
level = int(level_str)
if current_count + 1 >= level > current_count:
# 如果添加这个棋子后能激活新的等级
weight = self.weights_config.get('synergy_weights', {}).get(synergy.get('name', ''), 1.0)
level_multiplier = float(self.weights_config.get('synergy_level_weights', {}).get(level_str, 1.0))
value += weight * level_multiplier * 10
break
# 即使不能激活新等级,也增加一些价值
value += 1
# 添加棋子自定义权重
chess_weight = self.weights_config.get('chess_weights', {}).get(chess.get('displayName', ''), 1.0)
value *= chess_weight
chess_values.append((chess, value))
# 按价值从高到低排序
chess_values.sort(key=lambda x: x[1], reverse=True)
# 创建候选阵容,添加价值最高的棋子
candidate = TeamComposition()
# 添加基础阵容的棋子
for chess in base_team.chess_list:
candidate.add_chess(chess)
# 添加价值最高的剩余槽位数量的棋子
for i in range(min(remaining_slots, len(chess_values))):
candidate.add_chess(chess_values[i][0])
return [candidate]

View File

@ -4,20 +4,29 @@
from src.data_provider import DataQueryAPI from src.data_provider import DataQueryAPI
from src.recommendation import RecommendationEngine from src.recommendation import RecommendationEngine
from src.scoring import TeamScorer, ScoringConfig from src.scoring import TeamScorer, ScoringConfig
from src.config import get_global_weights_config
from typing import Optional
def main(): def main(config_path: Optional[str] = None):
"""主函数""" """
主函数
Args:
config_path: 配置文件路径用于全局权重配置
"""
print("=== 云顶之弈阵容推荐模块演示 ===") print("=== 云顶之弈阵容推荐模块演示 ===")
# 初始化数据查询API # 初始化数据查询API
print("\n正在加载数据...") print("\n正在加载数据...")
api = DataQueryAPI() api = DataQueryAPI()
# 获取全局权重配置
weights_config = get_global_weights_config(config_path)
# 初始化评分系统和推荐引擎 # 初始化评分系统和推荐引擎
scoring_config = ScoringConfig() scorer = TeamScorer(config_path=config_path)
scorer = TeamScorer(config=scoring_config) engine = RecommendationEngine(api=api, scorer=scorer, config_path=config_path)
engine = RecommendationEngine(api=api, scorer=scorer)
# 显示数据基本信息 # 显示数据基本信息
print(f"\n当前游戏版本: {api.data_loader.get_latest_version()}") print(f"\n当前游戏版本: {api.data_loader.get_latest_version()}")
@ -25,6 +34,13 @@ def main():
print(f"特质数量: {len(api.get_all_races())}") print(f"特质数量: {len(api.get_all_races())}")
print(f"棋子数量: {len(api.get_all_chess())}") print(f"棋子数量: {len(api.get_all_chess())}")
# 显示当前权重配置
base_weights = weights_config.get_base_weights()
print("\n当前评分权重配置:")
print(f" 羁绊等级权重: {base_weights.get('synergy_level_weight', 1.0)}")
print(f" 羁绊数量权重: {base_weights.get('synergy_count_weight', 0.5)}")
print(f" 棋子费用权重: {base_weights.get('chess_cost_weight', 0.1)}")
# 互动推荐示例 # 互动推荐示例
while True: while True:
print("\n请选择推荐方式:") print("\n请选择推荐方式:")
@ -32,9 +48,10 @@ def main():
print("2. 指定必选羁绊") print("2. 指定必选羁绊")
print("3. 指定必选棋子") print("3. 指定必选棋子")
print("4. 综合条件推荐") print("4. 综合条件推荐")
print("5. 退出") print("5. 调整权重配置")
print("6. 退出")
choice = input("请输入选项 (1-5): ") choice = input("请输入选项 (1-6): ")
if choice == '1': if choice == '1':
try: try:
@ -56,6 +73,20 @@ def main():
print(f"未找到名为 '{synergy_name}' 的羁绊") print(f"未找到名为 '{synergy_name}' 的羁绊")
continue continue
# 显示羁绊在配置中的权重
synergy_weight = weights_config.get_synergy_weights().get(synergy_name, 1.0)
print(f"羁绊 {synergy_name} 的当前权重为: {synergy_weight}")
# 可选:调整羁绊权重
adjust = input(f"是否调整该羁绊的权重?(y/n当前: {synergy_weight}): ").strip().lower()
if adjust == 'y':
try:
new_weight = float(input(f"请输入新权重值 (当前: {synergy_weight}): ").strip())
weights_config.set_synergy_weight(synergy_name, new_weight)
print(f"已将羁绊 {synergy_name} 的权重设置为 {new_weight}")
except ValueError:
print("请输入有效的数字")
try: try:
level = int(input(f"请输入 {synergy_name} 的目标等级 (默认为1): ") or "1") level = int(input(f"请输入 {synergy_name} 的目标等级 (默认为1): ") or "1")
required_synergies = [{'name': synergy_name, 'level': level}] required_synergies = [{'name': synergy_name, 'level': level}]
@ -81,6 +112,20 @@ def main():
print(f"未找到名为 '{chess_name}' 的棋子") print(f"未找到名为 '{chess_name}' 的棋子")
continue continue
# 显示棋子在配置中的权重
chess_weight = weights_config.get_chess_weights().get(chess_name, 1.0)
print(f"棋子 {chess_name} 的当前权重为: {chess_weight}")
# 可选:调整棋子权重
adjust = input(f"是否调整该棋子的权重?(y/n当前: {chess_weight}): ").strip().lower()
if adjust == 'y':
try:
new_weight = float(input(f"请输入新权重值 (当前: {chess_weight}): ").strip())
weights_config.set_chess_weight(chess_name, new_weight)
print(f"已将棋子 {chess_name} 的权重设置为 {new_weight}")
except ValueError:
print("请输入有效的数字")
try: try:
population = int(input("请输入阵容人口数 (1-10默认为9): ") or "9") population = int(input("请输入阵容人口数 (1-10默认为9): ") or "9")
if population < 1 or population > 10: if population < 1 or population > 10:
@ -112,6 +157,20 @@ def main():
print(f"未找到名为 '{synergy_name}' 的羁绊") print(f"未找到名为 '{synergy_name}' 的羁绊")
continue continue
# 显示羁绊在配置中的权重
synergy_weight = weights_config.get_synergy_weights().get(synergy_name, 1.0)
print(f"羁绊 {synergy_name} 的当前权重为: {synergy_weight}")
# 可选:调整羁绊权重
adjust = input(f"是否调整该羁绊的权重?(y/n当前: {synergy_weight}): ").strip().lower()
if adjust == 'y':
try:
new_weight = float(input(f"请输入新权重值 (当前: {synergy_weight}): ").strip())
weights_config.set_synergy_weight(synergy_name, new_weight)
print(f"已将羁绊 {synergy_name} 的权重设置为 {new_weight}")
except ValueError:
print("请输入有效的数字")
level = int(input(f"请输入 {synergy_name} 的目标等级 (默认为1): ") or "1") level = int(input(f"请输入 {synergy_name} 的目标等级 (默认为1): ") or "1")
required_synergies.append({'name': synergy_name, 'level': level}) required_synergies.append({'name': synergy_name, 'level': level})
@ -127,6 +186,20 @@ def main():
print(f"未找到名为 '{chess_name}' 的棋子") print(f"未找到名为 '{chess_name}' 的棋子")
continue continue
# 显示棋子在配置中的权重
chess_weight = weights_config.get_chess_weights().get(chess_name, 1.0)
print(f"棋子 {chess_name} 的当前权重为: {chess_weight}")
# 可选:调整棋子权重
adjust = input(f"是否调整该棋子的权重?(y/n当前: {chess_weight}): ").strip().lower()
if adjust == 'y':
try:
new_weight = float(input(f"请输入新权重值 (当前: {chess_weight}): ").strip())
weights_config.set_chess_weight(chess_name, new_weight)
print(f"已将棋子 {chess_name} 的权重设置为 {new_weight}")
except ValueError:
print("请输入有效的数字")
required_chess.append({'name': chess_name}) required_chess.append({'name': chess_name})
# 设置人口 # 设置人口
@ -147,6 +220,72 @@ def main():
print("请输入有效的整数") print("请输入有效的整数")
elif choice == '5': elif choice == '5':
print("\n=== 调整权重配置 ===")
print("1. 调整基础权重")
print("2. 调整羁绊权重")
print("3. 调整棋子权重")
print("4. 返回主菜单")
sub_choice = input("请输入选项 (1-4): ")
if sub_choice == '1':
try:
print("\n当前基础权重:")
base_weights = weights_config.get_base_weights()
for key, value in base_weights.items():
print(f" {key}: {value}")
param = input("\n请输入要调整的参数名称 (例如: synergy_level_weight): ")
if param not in base_weights:
print(f"参数 '{param}' 不存在")
continue
new_value = float(input(f"请输入新的权重值 (当前: {base_weights.get(param, 1.0)}): "))
weights_config.set_base_weight(param, new_value)
print(f"已将 {param} 的权重设置为 {new_value}")
# 更新评分系统
scorer.reload_config()
except ValueError:
print("请输入有效的数字")
elif sub_choice == '2':
synergy_name = input("请输入羁绊名称: ")
synergy = api.get_synergy_by_name(synergy_name)
if not synergy:
print(f"未找到名为 '{synergy_name}' 的羁绊")
continue
try:
current_weight = weights_config.get_synergy_weights().get(synergy_name, 1.0)
new_weight = float(input(f"请输入新的权重值 (当前: {current_weight}): "))
weights_config.set_synergy_weight(synergy_name, new_weight)
print(f"已将羁绊 {synergy_name} 的权重设置为 {new_weight}")
except ValueError:
print("请输入有效的数字")
elif sub_choice == '3':
chess_name = input("请输入棋子名称: ")
chess = api.get_chess_by_name(chess_name)
if not chess:
print(f"未找到名为 '{chess_name}' 的棋子")
continue
try:
current_weight = weights_config.get_chess_weights().get(chess_name, 1.0)
new_weight = float(input(f"请输入新的权重值 (当前: {current_weight}): "))
weights_config.set_chess_weight(chess_name, new_weight)
print(f"已将棋子 {chess_name} 的权重设置为 {new_weight}")
except ValueError:
print("请输入有效的数字")
elif sub_choice == '4':
continue
else:
print("无效选项,请重新输入")
elif choice == '6':
break break
else: else:
@ -200,11 +339,10 @@ def _display_recommended_teams(teams):
if race: if race:
race_names.append(race['name']) race_names.append(race['name'])
synergy_text = ' / '.join(job_names + race_names) job_str = ', '.join(job_names) if job_names else ''
print(f" - {chess['displayName']} ({price}费): {synergy_text}") race_str = ', '.join(race_names) if race_names else ''
# 显示阵容总费用 print(f" - {chess['displayName']} ({price}金币) | 职业: {job_str} | 特质: {race_str}")
print(f"\n阵容总费用: {team.total_cost}")
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,7 +1,8 @@
""" """
阵容评分模块 阵容评分模块 - 提供对云顶之弈阵容的评分功能
此模块包含评分系统相关的组件用于对阵容进行综合评分
""" """
from .scoring_system import TeamScorer, ScoringConfig from .scoring_system import TeamScorer, ScoringConfig
from .config_loader import ConfigLoader, create_default_config
__all__ = ['TeamScorer', 'ScoringConfig', 'ConfigLoader', 'create_default_config'] __all__ = ['TeamScorer', 'ScoringConfig']

View File

@ -1,133 +0,0 @@
"""
配置加载器 - 用于从外部配置文件加载自定义权重设置
此模块提供了从YAML或JSON配置文件中加载自定义权重设置的功能
支持为特定羁绊和棋子设置自定义权重
"""
import os
import json
import yaml
import logging
from typing import Dict, Any, Optional
# 配置日志
logger = logging.getLogger("TFT-Strategist-ConfigLoader")
class ConfigLoader:
"""
配置加载器用于从外部文件加载自定义权重设置
"""
def __init__(self, config_path: Optional[str] = None):
"""
初始化配置加载器
Args:
config_path: 配置文件路径如果为None则使用默认路径
"""
self.config_path = config_path or os.path.join("data", "weights_config.yaml")
self.config_data = {}
def load_config(self) -> Dict[str, Any]:
"""
加载配置文件
Returns:
Dict[str, Any]: 包含配置数据的字典
"""
if not os.path.exists(self.config_path):
logger.warning(f"配置文件不存在: {self.config_path}")
return {}
try:
file_ext = os.path.splitext(self.config_path)[1].lower()
if file_ext == '.json':
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config_data = json.load(f)
elif file_ext in ['.yaml', '.yml']:
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config_data = yaml.safe_load(f)
else:
logger.error(f"不支持的配置文件格式: {file_ext}")
return {}
logger.info(f"成功加载配置文件: {self.config_path}")
return self.config_data
except Exception as e:
logger.error(f"加载配置文件失败: {str(e)}")
return {}
def get_synergy_weights(self) -> Dict[str, float]:
"""
获取羁绊权重配置
Returns:
Dict[str, float]: 羁绊名称到权重的映射
"""
return self.config_data.get('synergy_weights', {})
def get_chess_weights(self) -> Dict[str, float]:
"""
获取棋子权重配置
Returns:
Dict[str, float]: 棋子名称到权重的映射
"""
return self.config_data.get('chess_weights', {})
def get_base_weights(self) -> Dict[str, float]:
"""
获取基础权重配置
Returns:
Dict[str, float]: 基础权重参数到权重值的映射
"""
return self.config_data.get('base_weights', {})
def create_default_config(config_path: str) -> None:
"""
创建默认配置文件
Args:
config_path: 配置文件路径
"""
default_config = {
'base_weights': {
'synergy_level_weight': 1.0,
'synergy_count_weight': 0.5,
'chess_cost_weight': 0.1
},
'synergy_weights': {
'重装战士': 1.5,
'魔法师': 1.2,
'神谕者': 1.3,
'斗士': 1.1
},
'chess_weights': {
'亚索': 1.5,
'艾希': 1.2,
'璐璐': 1.3,
'金克斯': 1.4
}
}
try:
file_ext = os.path.splitext(config_path)[1].lower()
os.makedirs(os.path.dirname(config_path), exist_ok=True)
if file_ext == '.json':
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(default_config, f, ensure_ascii=False, indent=2)
elif file_ext in ['.yaml', '.yml']:
with open(config_path, 'w', encoding='utf-8') as f:
yaml.dump(default_config, f, allow_unicode=True, default_flow_style=False)
else:
logger.error(f"不支持的配置文件格式: {file_ext}")
return
logger.info(f"成功创建默认配置文件: {config_path}")
except Exception as e:
logger.error(f"创建默认配置文件失败: {str(e)}")

View File

@ -12,7 +12,8 @@ from typing import Dict, List, Any, Optional
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
import os import os
from .config_loader import ConfigLoader, create_default_config from src.config import get_global_weights_config
from src.data_provider import DataQueryAPI
# 配置日志 # 配置日志
logging.basicConfig( logging.basicConfig(
@ -35,10 +36,10 @@ class ScoringConfig:
# 棋子费用权重倍数 # 棋子费用权重倍数
cost_multipliers: Dict[str, float] = None cost_multipliers: Dict[str, float] = None
# 自定义羁绊权重 (新增) # 自定义羁绊权重
synergy_weights: Dict[str, float] = None synergy_weights: Dict[str, float] = None
# 自定义棋子权重 (新增) # 自定义棋子权重
chess_weights: Dict[str, float] = None chess_weights: Dict[str, float] = None
def __post_init__(self): def __post_init__(self):
@ -80,56 +81,81 @@ class TeamScorer:
阵容评分系统负责对阵容进行综合评分 阵容评分系统负责对阵容进行综合评分
""" """
def __init__(self, config: Optional[ScoringConfig] = None, config_path: Optional[str] = None): def __init__(self, api: Optional[DataQueryAPI] = None, config_path: Optional[str] = None, config_obj: Optional[Dict[str, Any]] = None):
""" """
初始化阵容评分系统 初始化阵容评分系统
Args: Args:
config: 评分配置如果为None则使用默认配置 api: 数据查询API
config_path: 外部配置文件路径如果提供则从中加载自定义权重 config_path: 外部配置文件路径如果提供则从中加载自定义权重
config_obj: 配置对象优先于config_path使用
""" """
self.config = config if config else ScoringConfig() self.api = api if api else DataQueryAPI()
# 如果提供了配置文件路径,则加载自定义权重 # 加载权重配置
if config_path or os.path.exists(os.path.join("data", "weights_config.yaml")): if config_obj:
self._load_custom_weights(config_path) self.weights_config = config_obj
else:
self.weights_config = get_global_weights_config(config_path).config_data
# 加载基础权重参数
self.synergy_level_weight = self.weights_config.get('base_weights', {}).get('synergy_level_weight', 1.0)
self.synergy_count_weight = self.weights_config.get('base_weights', {}).get('synergy_count_weight', 0.5)
self.chess_cost_weight = self.weights_config.get('base_weights', {}).get('chess_cost_weight', 0.1)
# 加载自定义权重
self.synergy_weights = self.weights_config.get('synergy_weights', {})
self.chess_weights = self.weights_config.get('chess_weights', {})
# 加载羁绊等级权重和棋子费用权重倍数
synergy_level_weights = self.weights_config.get('synergy_level_weights', {})
cost_weights = self.weights_config.get('cost_weights', {})
# 将键转换为正确的类型
self.level_multipliers = {int(k) if isinstance(k, str) else k: float(v) for k, v in synergy_level_weights.items()}
self.cost_multipliers = {k: float(v) for k, v in cost_weights.items()}
def _load_custom_weights(self, config_path: Optional[str] = None) -> None: def reload_config(self) -> None:
""" """重新加载配置"""
从配置文件加载自定义权重 if isinstance(self.weights_config, dict):
# 针对直接传入的配置对象,不需要重新加载
Args: # 重新加载基础权重参数
config_path: 配置文件路径如果为None则使用默认路径 self.synergy_level_weight = self.weights_config.get('base_weights', {}).get('synergy_level_weight', 1.0)
""" self.synergy_count_weight = self.weights_config.get('base_weights', {}).get('synergy_count_weight', 0.5)
loader = ConfigLoader(config_path) self.chess_cost_weight = self.weights_config.get('base_weights', {}).get('chess_cost_weight', 0.1)
config_data = loader.load_config()
# 重新加载自定义权重
if not config_data: self.synergy_weights = self.weights_config.get('synergy_weights', {})
# 如果配置文件不存在或加载失败,创建默认配置文件 self.chess_weights = self.weights_config.get('chess_weights', {})
default_path = config_path or os.path.join("data", "weights_config.yaml")
create_default_config(default_path) # 重新加载羁绊等级权重和棋子费用权重倍数
logger.info(f"已创建默认配置文件: {default_path}") synergy_level_weights = self.weights_config.get('synergy_level_weights', {})
loader = ConfigLoader(default_path) cost_weights = self.weights_config.get('cost_weights', {})
config_data = loader.load_config()
# 将键转换为正确的类型
# 更新基础权重 self.level_multipliers = {int(k) if isinstance(k, str) else k: float(v) for k, v in synergy_level_weights.items()}
base_weights = loader.get_base_weights() self.cost_multipliers = {k: float(v) for k, v in cost_weights.items()}
for key, value in base_weights.items(): else:
if hasattr(self.config, key): # 获取全局配置的最新实例
setattr(self.config, key, value) config = get_global_weights_config()
logger.info(f"从配置文件加载基础权重: {key} = {value}") self.weights_config = config.config_data
# 更新羁绊自定义权重 # 重新加载基础权重参数
synergy_weights = loader.get_synergy_weights() self.synergy_level_weight = config.get_base_weights().get('synergy_level_weight', 1.0)
if synergy_weights: self.synergy_count_weight = config.get_base_weights().get('synergy_count_weight', 0.5)
self.config.synergy_weights.update(synergy_weights) self.chess_cost_weight = config.get_base_weights().get('chess_cost_weight', 0.1)
logger.info(f"从配置文件加载羁绊权重: {len(synergy_weights)}")
# 重新加载自定义权重
# 更新棋子自定义权重 self.synergy_weights = config.get_synergy_weights()
chess_weights = loader.get_chess_weights() self.chess_weights = config.get_chess_weights()
if chess_weights:
self.config.chess_weights.update(chess_weights) # 重新加载羁绊等级权重和棋子费用权重倍数
logger.info(f"从配置文件加载棋子权重: {len(chess_weights)}") synergy_level_weights = config.get_synergy_level_weights()
cost_weights = config.get_cost_weights()
# 将键转换为正确的类型
self.level_multipliers = {int(k) if isinstance(k, str) else k: float(v) for k, v in synergy_level_weights.items()}
self.cost_multipliers = {k: float(v) for k, v in cost_weights.items()}
def score_team(self, team) -> float: def score_team(self, team) -> float:
""" """
@ -150,15 +176,15 @@ class TeamScorer:
# 3. 棋子费用评分 # 3. 棋子费用评分
chess_cost_score = self._score_chess_cost(team) chess_cost_score = self._score_chess_cost(team)
# 4. 自定义羁绊和棋子权重评分 (新增) # 4. 自定义羁绊和棋子权重评分
custom_weight_score = self._score_custom_weights(team) custom_weight_score = self._score_custom_weights(team)
# 5. 阵容整体评分 # 5. 阵容整体评分
# 综合以上四项,加权求和 # 综合以上四项,加权求和
total_score = ( total_score = (
synergy_level_score * self.config.synergy_level_weight + synergy_level_score * self.synergy_level_weight +
synergy_count_score * self.config.synergy_count_weight + synergy_count_score * self.synergy_count_weight +
chess_cost_score * self.config.chess_cost_weight + chess_cost_score * self.chess_cost_weight +
custom_weight_score # 自定义权重分数已经包含各自的权重 custom_weight_score # 自定义权重分数已经包含各自的权重
) )
@ -181,27 +207,26 @@ class TeamScorer:
level = job_level['level'] level = job_level['level']
count = job_level['count'] count = job_level['count']
# 计算该羁绊的得分,根据等级和数量 # 计算该羁绊的得分,根据等级和数量
level_multiplier = self.config.level_multipliers.get(level, 1.0) level_multiplier = self.level_multipliers.get(level, 1.0)
# 额外激活的羁绊单位也计入评分 # 额外激活的羁绊单位也计入评分
extra_units = max(0, count - level) extra_units = max(0, count - level)
level_score = level * level_multiplier + extra_units * 0.5 score += level * level_multiplier + extra_units * 0.1
score += level_score
# 处理特质羁绊 # 处理特质羁绊
for race_level in team.synergy_levels.get('race', []): for race_level in team.synergy_levels.get('race', []):
level = race_level['level'] level = race_level['level']
count = race_level['count'] count = race_level['count']
# 计算该羁绊的得分 # 计算该羁绊的得分,根据等级和数量
level_multiplier = self.config.level_multipliers.get(level, 1.0) level_multiplier = self.level_multipliers.get(level, 1.0)
# 额外激活的羁绊单位也计入评分
extra_units = max(0, count - level) extra_units = max(0, count - level)
level_score = level * level_multiplier + extra_units * 0.5 score += level * level_multiplier + extra_units * 0.1
score += level_score
return score return score
def _score_synergy_count(self, team) -> float: def _score_synergy_count(self, team) -> float:
""" """
评分羁绊种类数量 评分激活的羁绊种类数量
Args: Args:
team: TeamComposition实例 team: TeamComposition实例
@ -209,17 +234,13 @@ class TeamScorer:
Returns: Returns:
float: 羁绊种类数量评分 float: 羁绊种类数量评分
""" """
# 计算激活的羁绊种类数量 # 获取激活的职业和特质数量
job_count = len(team.synergy_levels.get('job', [])) active_job_count = len(team.synergy_levels.get('job', []))
race_count = len(team.synergy_levels.get('race', [])) active_race_count = len(team.synergy_levels.get('race', []))
# 根据羁绊种类数量评分 # 根据激活的羁绊种类数量计算得分
# 羁绊越多,评分越高,但有一个合理的上限 # 激活的羁绊种类越多,得分越高,但增长是非线性的
total_count = job_count + race_count return (active_job_count + active_race_count) ** 0.8
# 使用对数函数避免羁绊数量过多时分数过高
score = (1 + total_count) ** 0.8
return score
def _score_chess_cost(self, team) -> float: def _score_chess_cost(self, team) -> float:
""" """
@ -233,21 +254,18 @@ class TeamScorer:
""" """
score = 0.0 score = 0.0
# 按照棋子费用评 # 根据棋子费用计算得
for chess in team.chess_list: for chess in team.chess_list:
cost = chess.get('price', '1') cost = str(chess.get('price', '1'))
cost_multiplier = self.config.cost_multipliers.get(cost, 1.0) # 费用越高的棋子,权重越大
cost_multiplier = self.cost_multipliers.get(cost, 1.0)
score += float(cost) * cost_multiplier score += float(cost) * cost_multiplier
# 费用分布的平衡性评分
# 根据阵容人口,期望的费用分布可能不同
# 这里可以进一步完善
return score return score
def _score_custom_weights(self, team) -> float: def _score_custom_weights(self, team) -> float:
""" """
根据自定义权重评分 根据自定义权重对阵容进行评分
Args: Args:
team: TeamComposition实例 team: TeamComposition实例
@ -257,79 +275,140 @@ class TeamScorer:
""" """
score = 0.0 score = 0.0
# 应用自定义羁绊权重 # 1. 评分羁绊自定义权重
if self.config.synergy_weights: for job_level in team.synergy_levels.get('job', []):
# 职业羁绊 synergy_name = job_level['name']
for job_level in team.synergy_levels.get('job', []): if synergy_name in self.synergy_weights:
synergy_name = job_level.get('name', '') weight = self.synergy_weights[synergy_name]
if synergy_name in self.config.synergy_weights: score += weight * job_level['level']
weight = self.config.synergy_weights[synergy_name]
level = job_level['level']
score += level * weight
logger.debug(f"应用羁绊权重: {synergy_name}, 等级: {level}, 权重: {weight}")
# 特质羁绊
for race_level in team.synergy_levels.get('race', []):
synergy_name = race_level.get('name', '')
if synergy_name in self.config.synergy_weights:
weight = self.config.synergy_weights[synergy_name]
level = race_level['level']
score += level * weight
logger.debug(f"应用羁绊权重: {synergy_name}, 等级: {level}, 权重: {weight}")
# 应用自定义棋子权重 for race_level in team.synergy_levels.get('race', []):
if self.config.chess_weights: synergy_name = race_level['name']
for chess in team.chess_list: if synergy_name in self.synergy_weights:
chess_name = chess.get('displayName', '') weight = self.synergy_weights[synergy_name]
if chess_name in self.config.chess_weights: score += weight * race_level['level']
weight = self.config.chess_weights[chess_name]
score += weight # 2. 评分棋子自定义权重
logger.debug(f"应用棋子权重: {chess_name}, 权重: {weight}") for chess in team.chess_list:
chess_name = chess.get('displayName', '')
if chess_name in self.chess_weights:
weight = self.chess_weights[chess_name]
score += weight
return score return score
def customize_scoring(self, **kwargs) -> None: def customize_scoring(self, **kwargs) -> None:
""" """
自定义评分参数 自定义评分参数
Args: Args:
**kwargs: 评分参数如synergy_level_weight, chess_cost_weight等 **kwargs: 参数名和值的映射
""" """
for key, value in kwargs.items(): for key, value in kwargs.items():
if hasattr(self.config, key): if hasattr(self, key):
setattr(self.config, key, value) setattr(self, key, value)
logger.info(f"设置评分参数 {key} = {value}") logger.info(f"自定义评分参数: {key} = {value}")
else:
logger.warning(f"未知的评分参数: {key}")
def set_synergy_weight(self, synergy_name: str, weight: float) -> None: def set_synergy_weight(self, synergy_name: str, weight: float) -> None:
""" """
设置特定羁绊的自定义权重 设置特定羁绊的权重
Args: Args:
synergy_name: 羁绊名称 synergy_name: 羁绊名称
weight: 权重值 weight: 权重值
""" """
self.config.synergy_weights[synergy_name] = weight self.synergy_weights[synergy_name] = weight
logger.info(f"设置羁绊权重: {synergy_name} = {weight}") # 同时更新全局配置
weights_config = get_global_weights_config()
weights_config.set_synergy_weight(synergy_name, weight)
def set_chess_weight(self, chess_name: str, weight: float) -> None: def set_chess_weight(self, chess_name: str, weight: float) -> None:
""" """
设置特定棋子的自定义权重 设置特定棋子的权重
Args: Args:
chess_name: 棋子名称 chess_name: 棋子名称
weight: 权重值 weight: 权重值
""" """
self.config.chess_weights[chess_name] = weight self.chess_weights[chess_name] = weight
logger.info(f"设置棋子权重: {chess_name} = {weight}") # 同时更新全局配置
weights_config = get_global_weights_config()
weights_config.set_chess_weight(chess_name, weight)
def reload_config(self, config_path: Optional[str] = None) -> None: def set_synergy_level_weight(self, level: int, weight: float) -> None:
""" """
新加载配置文件 设置特定羁绊等级的权
Args: Args:
config_path: 配置文件路径如果为None则使用默认路径 level: 羁绊等级
weight: 权重值
""" """
self._load_custom_weights(config_path) self.level_multipliers[level] = weight
logger.info("重新加载配置文件完成") # 同时更新全局配置
weights_config = get_global_weights_config()
weights_config.set_synergy_level_weight(str(level), weight)
def set_cost_weight(self, cost: str, weight: float) -> None:
"""
设置特定棋子费用的权重
Args:
cost: 棋子费用
weight: 权重值
"""
self.cost_multipliers[cost] = weight
# 同时更新全局配置
weights_config = get_global_weights_config()
weights_config.set_cost_weight(cost, weight)
def set_level_multiplier(self, level: int, multiplier: float) -> None:
"""
设置特定羁绊等级的权重倍数
Args:
level: 羁绊等级
multiplier: 权重倍数
"""
self.level_multipliers[level] = multiplier
# 同时更新全局配置
weights_config = get_global_weights_config()
weights_config.set_level_multiplier(level, multiplier)
def set_cost_multiplier(self, cost: str, multiplier: float) -> None:
"""
设置特定棋子费用的权重倍数
Args:
cost: 棋子费用
multiplier: 权重倍数
"""
self.cost_multipliers[cost] = multiplier
# 同时更新全局配置
weights_config = get_global_weights_config()
weights_config.set_cost_multiplier(cost, multiplier)
def set_synergy_level_multiplier(self, level: int, multiplier: float) -> None:
"""
设置特定羁绊等级权重倍数
Args:
level: 羁绊等级
multiplier: 权重倍数
"""
self.level_multipliers[level] = multiplier
# 同时更新全局配置
weights_config = get_global_weights_config()
weights_config.set_synergy_level_multiplier(level, multiplier)
def set_cost_level_multiplier(self, level: int, multiplier: float) -> None:
"""
设置特定羁绊等级费用权重倍数
Args:
level: 羁绊等级
multiplier: 权重倍数
"""
self.level_multipliers[level] = multiplier
# 同时更新全局配置
weights_config = get_global_weights_config()
weights_config.set_cost_level_multiplier(level, multiplier)

117
src/test_scoring.py Normal file
View File

@ -0,0 +1,117 @@
"""
测试评分系统的配置加载功能
"""
import sys
import os
import logging
from dataclasses import dataclass
from typing import Dict, List, Optional
# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.scoring.scoring_system import TeamScorer
from src.config import get_global_weights_config
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("TFT-Strategist-TestScoring")
@dataclass
class MockTeam:
"""模拟的阵容类,用于测试"""
chess_list: List[Dict]
synergy_levels: Dict
def main(config_path: Optional[str] = None):
"""
主函数
Args:
config_path: 配置文件路径用于全局权重配置
"""
logger.info("开始测试评分系统...")
# 加载全局权重配置
weights_config = get_global_weights_config(config_path)
# 创建评分器实例
scorer = TeamScorer(config_path=config_path)
# 打印全局配置
logger.info("基础权重:")
base_weights = weights_config.get_base_weights()
for param, value in base_weights.items():
logger.info(f" {param}: {value}")
logger.info("羁绊等级倍数:")
level_weights = weights_config.get_synergy_level_weights()
for level, multiplier in sorted(level_weights.items(), key=lambda x: int(x[0])):
logger.info(f" 等级 {level}: {multiplier}")
logger.info("棋子费用倍数:")
cost_weights = weights_config.get_cost_weights()
for cost, multiplier in sorted(cost_weights.items(), key=lambda x: int(x[0])):
logger.info(f" 费用 {cost}: {multiplier}")
logger.info("羁绊自定义权重 (前5个):")
synergy_weights = weights_config.get_synergy_weights()
for name, weight in list(sorted(synergy_weights.items()))[:5]:
logger.info(f" {name}: {weight}")
logger.info("棋子自定义权重 (前5个):")
chess_weights = weights_config.get_chess_weights()
for name, weight in list(sorted(chess_weights.items()))[:5]:
logger.info(f" {name}: {weight}")
# 测试修改权重
logger.info("\n测试修改权重:")
test_synergy = next(iter(synergy_weights.keys()))
original_weight = synergy_weights.get(test_synergy, 1.0)
new_weight = original_weight + 0.5
logger.info(f"将羁绊 {test_synergy} 的权重从 {original_weight} 修改为 {new_weight}")
weights_config.set_synergy_weight(test_synergy, new_weight)
# 确认修改生效
updated_weight = weights_config.get_synergy_weights().get(test_synergy)
logger.info(f"修改后的权重: {updated_weight}")
# 重新加载评分系统配置
scorer.reload_config()
# 测试评分函数
mock_team = MockTeam(
chess_list=[
{"displayName": "佛耶戈", "price": "4"},
{"displayName": "厄加特", "price": "5"},
{"displayName": "盖伦", "price": "3"},
{"displayName": "扎克", "price": "3"}
],
synergy_levels={
"job": [
{"name": "重装战士", "level": 2, "count": 2},
{"name": "斗士", "level": 2, "count": 2}
],
"race": [
{"name": "鳄霸", "level": 1, "count": 1}
]
}
)
score = scorer.score_team(mock_team)
logger.info(f"模拟阵容评分: {score}")
# 恢复原来的权重
logger.info(f"将羁绊 {test_synergy} 的权重恢复为 {original_weight}")
weights_config.set_synergy_weight(test_synergy, original_weight)
# 重新评分
scorer.reload_config()
score = scorer.score_team(mock_team)
logger.info(f"恢复权重后的评分: {score}")
logger.info("测试完成!")
if __name__ == "__main__":
main()

8
src/web/__init__.py Normal file
View File

@ -0,0 +1,8 @@
"""
云顶之弈阵容推荐器 - Web模块
提供Web界面允许用户通过浏览器调整权重进行阵容推荐
"""
from src.web.app import create_app, run_server
__all__ = ['create_app', 'run_server']

389
src/web/app.py Normal file
View File

@ -0,0 +1,389 @@
"""
云顶之弈阵容推荐器 - Web应用
"""
import os
import json
import logging
import copy
from flask import Flask, render_template, request, jsonify
from flask_cors import CORS
from waitress import serve
from typing import Optional, Dict, Any
from src.data_provider import DataQueryAPI
from src.recommendation import RecommendationEngine
from src.scoring.scoring_system import TeamScorer
from src.config import get_global_weights_config, WeightsConfig
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("TFT-Strategist-Web")
def create_app(config_path: Optional[str] = None):
"""
创建Flask应用实例
Args:
config_path: 配置文件路径用于全局权重配置
"""
app = Flask(__name__,
static_folder='static',
template_folder='templates')
# 启用CORS允许跨域请求
CORS(app)
# 加载全局权重配置
weights_config = get_global_weights_config(config_path)
# 加载数据和组件
data_api = DataQueryAPI()
scorer = TeamScorer(config_path=config_path)
engine = RecommendationEngine(api=data_api, scorer=scorer, config_path=config_path)
# 原始默认配置,用于重置操作
default_config = copy.deepcopy(weights_config.config_data)
@app.route('/')
def index():
"""首页"""
# 获取所有羁绊和棋子信息,用于前端展示
all_jobs = data_api.get_all_jobs()
all_races = data_api.get_all_races()
all_synergies = all_jobs + all_races
all_chess = data_api.get_all_chess()
# 打印羁绊和棋子数据结构,用于调试
logger.info(f"羁绊数量: {len(all_synergies)}")
if all_synergies:
sample_synergy = all_synergies[0]
logger.info(f"羁绊示例: {sample_synergy}")
logger.info(f"棋子数量: {len(all_chess)}")
if all_chess:
sample_chess = all_chess[0]
logger.info(f"棋子示例: {sample_chess}")
# 获取全局配置中的权重信息
base_weights = weights_config.get_base_weights()
synergy_weights = weights_config.get_synergy_weights()
chess_weights = weights_config.get_chess_weights()
# 打印权重配置,用于调试
logger.info(f"羁绊权重示例: {list(synergy_weights.items())[:3]}")
logger.info(f"棋子权重示例: {list(chess_weights.items())[:3]}")
return render_template('index.html',
synergies=all_synergies,
chess=all_chess,
base_weights=base_weights,
synergy_weights=synergy_weights,
chess_weights=chess_weights)
@app.route('/api/recommend', methods=['POST'])
def recommend():
"""阵容推荐API"""
try:
# 获取用户提交的参数
data = request.json
population = data.get('population', 9)
num_results = data.get('num_results', 3)
# 获取用户配置的权重
base_weights = data.get('base_weights', {})
synergy_weights = data.get('synergy_weights', {})
chess_weights = data.get('chess_weights', {})
# 创建临时配置对象,不修改全局配置
temp_config = create_temp_config(
weights_config.config_data,
base_weights,
synergy_weights,
chess_weights
)
# 创建临时评分器
temp_scorer = TeamScorer(config_obj=temp_config)
# 创建临时推荐引擎
temp_engine = RecommendationEngine(api=data_api, scorer=temp_scorer, config_obj=temp_config)
# 获取必选棋子和必选羁绊
required_chess_ids = data.get('required_chess', [])
required_synergy_ids = data.get('required_synergies', [])
# 将ID转换为字典格式确保将ID转换为字符串
required_chess = [{'id': str(chess_id)} for chess_id in required_chess_ids]
required_synergies = [{'id': str(synergy_id)} for synergy_id in required_synergy_ids]
# 调用推荐引擎生成阵容
teams = temp_engine.recommend_team(
population=population,
required_synergies=required_synergies,
required_chess=required_chess,
max_results=num_results
)
# 将结果转换为JSON
results = []
for team in teams:
# 获取阵容信息
chess_list = []
for chess_obj in team.chess_list:
chess_id = chess_obj.get('chessId')
if chess_id:
chess_list.append({
'id': chess_id,
'name': chess_obj.get('displayName', ''),
'cost': chess_obj.get('price', 0),
'image': f"chess_{chess_id}.png" # 假设有对应图片
})
# 获取激活的羁绊
active_synergies = {}
# 合并job和race的激活羁绊
for synergy_type in ['job', 'race']:
for synergy_info in team.synergy_levels.get(synergy_type, []):
synergy_id = synergy_info.get('id')
if synergy_id:
# 取最高等级
if synergy_id not in active_synergies or active_synergies[synergy_id] < synergy_info.get('level', 0):
active_synergies[synergy_id] = synergy_info.get('level', 0)
# 转换为前端需要的格式
active_synergies_list = []
for synergy_id, level in active_synergies.items():
synergy_info = data_api.get_synergy_by_id(synergy_id)
if synergy_info:
name = synergy_info.get('name', '')
active_synergies_list.append({
'id': synergy_id,
'name': name,
'level': level,
'description': synergy_info.get('description', ''),
'image': f"synergy_{synergy_id}.png" # 假设有对应图片
})
results.append({
'chess_list': chess_list,
'active_synergies': active_synergies_list,
'score': team.score
})
return jsonify({
'status': 'success',
'results': results
})
except Exception as e:
logger.exception("推荐过程中发生错误")
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@app.route('/api/weights', methods=['GET'])
def get_weights():
"""获取当前权重配置API"""
return jsonify({
'base_weights': weights_config.get_base_weights(),
'synergy_weights': weights_config.get_synergy_weights(),
'chess_weights': weights_config.get_chess_weights(),
'synergy_level_weights': weights_config.get_synergy_level_weights(),
'cost_weights': weights_config.get_cost_weights()
})
@app.route('/api/weights', methods=['POST'])
def update_weights():
"""更新权重配置API"""
try:
data = request.json
weights_config.update_config_data(data)
return jsonify({'status': 'success'})
except Exception as e:
logger.exception("更新权重配置失败")
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@app.route('/api/details', methods=['GET'])
def get_details():
"""获取棋子或羁绊的详细信息API"""
try:
item_type = request.args.get('type') # 'chess' 或 'synergy'
item_id = request.args.get('id')
if not item_type or not item_id:
return jsonify({
'status': 'error',
'message': '缺少必要参数'
}), 400
# 获取详细信息
if item_type == 'chess':
# 获取棋子详情
chess = data_api.get_chess_by_id(item_id)
if not chess:
return jsonify({
'status': 'error',
'message': f'未找到ID为{item_id}的棋子'
}), 404
# 获取棋子的所有羁绊
synergies = data_api.get_synergies_of_chess(item_id)
# 构建详细信息
details = {
'id': chess.get('chessId'),
'name': chess.get('displayName', ''),
'cost': chess.get('price', 0),
'skill_name': chess.get('skillName', ''),
'skill_description': chess.get('skillDetail', ''),
'skill_introduce': chess.get('skillIntroduce', ''),
'skill_type': chess.get('skillType', ''),
'skill_mana': chess.get('magic', ''),
# 添加更多棋子属性 - 使用与 chess.json 一致的字段名
'life': chess.get('life', ''),
'attack': chess.get('attack', ''),
'attackSpeed': chess.get('attackSpeed', ''),
'attackRange': chess.get('attackRange', ''),
'armor': chess.get('armor', ''),
'spellBlock': chess.get('spellBlock', ''),
'magic': chess.get('magic', ''),
'startMagic': chess.get('startMagic', ''),
'crit': chess.get('crit', ''),
'attackMag': chess.get('attackMag', ''),
'synergies': [{
'id': synergy.get('jobId') or synergy.get('raceId'),
'name': synergy.get('name', ''),
'type': 'job' if 'jobId' in synergy else 'race'
} for synergy in synergies]
}
return jsonify({
'status': 'success',
'type': 'chess',
'details': details
})
elif item_type == 'synergy':
# 获取羁绊详情
synergy = data_api.get_synergy_by_id(item_id)
if not synergy:
return jsonify({
'status': 'error',
'message': f'未找到ID为{item_id}的羁绊'
}), 404
# 获取羁绊的所有等级信息
synergy_levels = synergy.get('level', {})
# 获取该羁绊的所有棋子
related_chess = data_api.get_chess_by_synergy(item_id)
# 构建详细信息
details = {
'id': synergy.get('jobId') or synergy.get('raceId'),
'name': synergy.get('name', ''),
'description': synergy.get('description', ''),
'introduce': synergy.get('introduce', ''),
'type': 'job' if 'jobId' in synergy else 'race',
'levels': [{
'level': level,
'effect': effect
} for level, effect in synergy_levels.items()],
# 添加相关棋子
'related_chess': [{
'id': chess.get('chessId'),
'name': chess.get('displayName', ''),
'cost': chess.get('price', 0)
} for chess in related_chess]
}
return jsonify({
'status': 'success',
'type': 'synergy',
'details': details
})
else:
return jsonify({
'status': 'error',
'message': f'不支持的类型: {item_type}'
}), 400
except Exception as e:
logger.exception("获取详细信息失败")
return jsonify({
'status': 'error',
'message': str(e)
}), 500
return app
def create_temp_config(base_config: Dict[str, Any],
base_weights: Dict[str, float],
synergy_weights: Dict[str, float],
chess_weights: Dict[str, float]) -> Dict[str, Any]:
"""
创建临时权重配置
Args:
base_config: 基础配置
base_weights: 基础权重
synergy_weights: 羁绊权重
chess_weights: 棋子权重
Returns:
Dict[str, Any]: 临时配置
"""
# 深拷贝基础配置,避免修改原始配置
temp_config = copy.deepcopy(base_config)
# 更新基础权重
if 'base_weights' not in temp_config:
temp_config['base_weights'] = {}
for param, value in base_weights.items():
temp_config['base_weights'][param] = value
# 更新羁绊权重
if 'synergy_weights' not in temp_config:
temp_config['synergy_weights'] = {}
for synergy_name, weight in synergy_weights.items():
temp_config['synergy_weights'][synergy_name] = weight
# 更新棋子权重
if 'chess_weights' not in temp_config:
temp_config['chess_weights'] = {}
for chess_name, weight in chess_weights.items():
temp_config['chess_weights'][chess_name] = weight
return temp_config
def run_server(host='0.0.0.0', port=5000, dev_mode=False, config_path=None):
"""
运行Web服务器
Args:
host: 服务器主机地址
port: 服务器端口
dev_mode: 是否启用开发模式
config_path: 配置文件路径
"""
app = create_app(config_path)
logger.info(f"启动Web服务器: {'开发模式' if dev_mode else '生产模式'}")
if dev_mode:
# 开发模式使用Flask内置服务器
app.run(host=host, port=port, debug=True)
else:
# 生产模式使用waitress
serve(app, host=host, port=port)

View File

@ -0,0 +1,369 @@
/* 云顶之弈阵容推荐器样式 */
/* 滑块样式 */
.weight-slider {
height: 6px;
border-radius: 3px;
margin-top: 5px;
margin-bottom: 15px;
}
.noUi-connect {
background: #4f46e5; /* Indigo-600 */
}
.noUi-handle {
height: 16px !important;
width: 16px !important;
border-radius: 50%;
background: #4f46e5;
box-shadow: none;
border: 2px solid #ffffff;
cursor: pointer;
}
.noUi-handle:before,
.noUi-handle:after {
display: none;
}
/* 棋子卡片样式 */
.chess-card {
border: 1px solid #e5e7eb;
border-radius: 0.375rem;
background-color: #f9fafb;
transition: all 0.2s;
}
.chess-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
/* 悬停提示样式 */
.chess-item, .synergy-item {
cursor: pointer;
position: relative;
}
/* 活跃状态样式 */
.chess-item.tooltip-active, .synergy-item.tooltip-active {
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.4);
z-index: 5;
}
/* 增强提示框样式 */
#tft-tooltip {
transition: all 0.3s ease;
max-height: 80vh;
overflow-y: auto;
overflow-x: hidden;
border: 1px solid rgba(99, 102, 241, 0.3);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
transform-origin: top left;
animation: tooltipFadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1);
scrollbar-width: thin;
scrollbar-color: rgba(79, 70, 229, 0.4) rgba(17, 24, 39, 0.2);
}
/* 自定义滚动条样式 */
#tft-tooltip::-webkit-scrollbar {
width: 6px;
}
#tft-tooltip::-webkit-scrollbar-track {
background: rgba(17, 24, 39, 0.1);
border-radius: 3px;
}
#tft-tooltip::-webkit-scrollbar-thumb {
background-color: rgba(79, 70, 229, 0.4);
border-radius: 3px;
transition: background-color 0.3s;
}
#tft-tooltip::-webkit-scrollbar-thumb:hover {
background-color: rgba(79, 70, 229, 0.6);
}
/* 提示框内容淡入效果 */
#tft-tooltip .tooltip-content {
word-break: break-word;
opacity: 0;
animation: contentFadeIn 0.2s ease forwards;
animation-delay: 0.1s;
}
@keyframes contentFadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes tooltipFadeIn {
from {
opacity: 0;
transform: translateY(-5px) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* 提示框中的费用标识 */
#tft-tooltip .cost-1 { border-left: 3px solid #94a3b8; } /* 1费 - 灰色 */
#tft-tooltip .cost-2 { border-left: 3px solid #65a30d; } /* 2费 - 绿色 */
#tft-tooltip .cost-3 { border-left: 3px solid #2563eb; } /* 3费 - 蓝色 */
#tft-tooltip .cost-4 { border-left: 3px solid #7e22ce; } /* 4费 - 紫色 */
#tft-tooltip .cost-5 { border-left: 3px solid #f59e0b; } /* 5费 - 金色 */
/* 提示框内交互元素样式 */
.chess-item-mini, .synergy-item-mini {
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.chess-item-mini:hover, .synergy-item-mini:hover {
background-color: rgba(255, 255, 255, 0.1);
transform: translateY(-1px);
}
.chess-item-mini:hover::after, .synergy-item-mini:hover::after {
content: '查看详情';
position: absolute;
top: -18px;
right: 5px;
background: rgba(79, 70, 229, 0.9);
color: white;
font-size: 8px;
padding: 2px 4px;
border-radius: 2px;
}
/* 提示框中的各部分样式 */
#tft-tooltip .tooltip-content {
word-break: break-word;
padding-top: 5px;
padding-right: 36px; /* 为右侧关闭按钮留出空间 */
}
/* 关闭按钮样式 */
#tft-tooltip .tooltip-close-btn {
transition: all 0.2s ease;
opacity: 0.9;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
#tft-tooltip .tooltip-close-btn:hover {
opacity: 1;
transform: scale(1.05);
}
/* 特殊介绍文本样式 */
#tft-tooltip .skill-introduce,
#tft-tooltip .synergy-introduce {
border-left: 3px solid rgba(79, 70, 229, 0.7);
background-color: rgba(79, 70, 229, 0.1);
padding-left: 8px;
}
#tft-tooltip .loading-spinner {
opacity: 0.8;
}
/* 优化悬停标识 */
.chess-item::after, .synergy-item::after {
content: '?';
position: absolute;
top: -5px;
right: -5px;
width: 16px;
height: 16px;
background: rgba(79, 70, 229, 0.9);
color: white;
border-radius: 50%;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s, transform 0.2s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 10;
}
.chess-item:hover::after, .synergy-item:hover::after {
opacity: 1;
transform: scale(1.1);
}
/* 羁绊标签样式 */
.synergy-tag {
background-color: #e0e7ff;
color: #4338ca;
border-radius: 9999px;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
display: inline-flex;
align-items: center;
}
/* 棋子标识 */
.chess-cost-1 { border-left: 3px solid #94a3b8; } /* 1费 - 灰色 */
.chess-cost-2 { border-left: 3px solid #65a30d; } /* 2费 - 绿色 */
.chess-cost-3 { border-left: 3px solid #2563eb; } /* 3费 - 蓝色 */
.chess-cost-4 { border-left: 3px solid #7e22ce; } /* 4费 - 紫色 */
.chess-cost-5 { border-left: 3px solid #f59e0b; } /* 5费 - 金色 */
/* 动画效果 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.team-result {
animation: fadeIn 0.3s ease-out;
}
/* 响应式调整 */
@media (max-width: 768px) {
.chess-list {
grid-template-columns: repeat(2, 1fr);
}
}
/* 激活状态样式 */
.synergy-weight-item.active,
.chess-weight-item.active {
background-color: #e0e7ff;
border-left: 3px solid #4f46e5;
padding-left: 5px;
border-radius: 4px;
}
/* 移动设备上的提示框样式优化 */
@media (max-width: 768px) {
#tft-tooltip {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(99, 102, 241, 0.5);
max-width: calc(100vw - 40px);
animation: mobileTooltipFadeIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
max-height: 70vh;
/* 固定位置在屏幕中央 */
position: fixed;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%);
margin: 0 auto;
width: calc(100vw - 40px) !important;
}
#tft-tooltip .tooltip-close-btn {
width: 38px;
height: 38px;
top: 10px;
right: 10px;
}
#tft-tooltip .tooltip-content {
margin-top: 10px;
padding-right: 20px;
padding-bottom: 5px;
}
/* 增大移动端触摸目标大小 */
.chess-item-mini, .synergy-item-mini {
padding: 12px;
margin-bottom: 10px;
position: relative;
overflow: hidden;
/* 增加右侧间距,避免文本与按钮重叠 */
padding-right: 80px;
}
/* 移动端使用更明显的交互提示 */
.chess-item-mini::after, .synergy-item-mini::after {
content: '点击查看';
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
color: white;
font-size: 10px;
background-color: rgba(79, 70, 229, 0.5);
padding: 3px 8px;
border-radius: 10px;
white-space: nowrap;
z-index: 5;
}
/* 防止棋子费用与点击按钮重叠 */
.chess-item-mini .text-xs.px-1\.5.py-0\.5.bg-gray-600.rounded {
margin-right: 65px;
}
/* 防止羁绊类型与点击按钮重叠 */
.synergy-item-mini .bg-gray-600.text-xs.px-1\.5.py-0\.5.rounded {
margin-right: 65px;
}
/* 移动端提示框内部元素间距优化 */
#tft-tooltip .mb-2 {
margin-bottom: 10px;
}
#tft-tooltip .mb-3 {
margin-bottom: 15px;
}
#tft-tooltip .p-2 {
padding: 10px;
}
/* 移动端动画效果 */
@keyframes mobileTooltipFadeIn {
from {
opacity: 0;
transform: translate(-50%, -45%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
/* 提示框内部滚动优化 */
#tft-tooltip::-webkit-scrollbar {
width: 5px;
}
/* 移动端加载状态优化 */
#tft-tooltip .loading-spinner .animate-spin {
height: 45px;
width: 45px;
border-width: 3px;
}
#tft-tooltip .loading-spinner .text-xs {
font-size: 12px;
margin-top: 8px;
}
/* 移动端表格布局优化 */
#tft-tooltip .grid.grid-cols-2 {
grid-template-columns: 1fr;
}
/* 改进相关棋子列表显示 */
#tft-tooltip .grid.grid-cols-2.gap-1.mt-1 {
grid-template-columns: 1fr 1fr;
}
}

1447
src/web/static/js/main.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>云顶之弈阵容推荐器</title>
<!-- 使用TailwindCSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- jQuery -->
<script src="https://unpkg.com/jquery@3.6.0/dist/jquery.min.js"></script>
<!-- noUiSlider和其依赖 -->
<link rel="stylesheet" href="https://unpkg.com/nouislider@15.5.1/dist/nouislider.min.css">
<script src="https://unpkg.com/nouislider@15.5.1/dist/nouislider.min.js"></script>
<!-- 自定义样式 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<style>
/* 抽屉样式 */
.drawer {
position: fixed;
top: 0;
right: -100%;
height: 100vh;
width: 320px;
background-color: white;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
transition: right 0.3s ease;
z-index: 1000;
overflow-y: auto;
padding: 1rem;
}
.drawer.open {
right: 0;
}
.drawer-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: none;
}
.drawer-backdrop.open {
display: block;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<!-- 抽屉背景遮罩 -->
<div class="drawer-backdrop" id="drawer-backdrop"></div>
<!-- 权重设置抽屉 -->
<div class="drawer" id="weights-drawer">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-indigo-700">权重设置</h2>
<button id="close-drawer" class="text-gray-500 hover:text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="mb-4">
<button id="reset-to-default" class="w-full bg-gray-200 text-gray-800 px-4 py-2 rounded-lg hover:bg-gray-300 transition-colors mb-4">
恢复默认配置
</button>
</div>
<h3 class="text-lg font-semibold mb-4 text-indigo-700">基础权重</h3>
<div class="mb-4">
<label class="block text-gray-700 mb-2">羁绊等级权重</label>
<div id="synergy-level-weight-slider" class="weight-slider"></div>
<div class="flex justify-between mt-1">
<span class="text-xs text-gray-500">0.0</span>
<span id="synergy-level-weight-value" class="text-xs text-gray-700">1.0</span>
<span class="text-xs text-gray-500">3.0</span>
</div>
</div>
<div class="mb-4">
<label class="block text-gray-700 mb-2">羁绊数量权重</label>
<div id="synergy-count-weight-slider" class="weight-slider"></div>
<div class="flex justify-between mt-1">
<span class="text-xs text-gray-500">0.0</span>
<span id="synergy-count-weight-value" class="text-xs text-gray-700">0.5</span>
<span class="text-xs text-gray-500">3.0</span>
</div>
</div>
<div class="mb-6">
<label class="block text-gray-700 mb-2">棋子费用权重</label>
<div id="chess-cost-weight-slider" class="weight-slider"></div>
<div class="flex justify-between mt-1">
<span class="text-xs text-gray-500">0.0</span>
<span id="chess-cost-weight-value" class="text-xs text-gray-700">0.1</span>
<span class="text-xs text-gray-500">1.0</span>
</div>
</div>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-indigo-700">羁绊权重设置</h3>
<div class="flex items-center">
<input type="checkbox" id="show-only-active" class="mr-2">
<label for="show-only-active" class="text-sm text-gray-700">仅显示已激活</label>
</div>
</div>
<div id="synergy-weights-container" class="space-y-4 mb-6">
{% for synergy_id, weight in synergy_weights.items() %}
<div class="synergy-weight-item" data-synergy-id="{{ synergy_id }}" data-synergy-name="{{ synergy_id }}">
<div class="flex items-center justify-between mb-1">
<label class="text-gray-700">{{ synergy_id }}</label>
<span class="synergy-weight-value text-xs text-gray-700">{{ weight }}</span>
</div>
<div class="synergy-weight-slider"></div>
</div>
{% endfor %}
</div>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-indigo-700">棋子权重设置</h3>
<div class="flex items-center">
<input type="checkbox" id="show-only-active-chess" class="mr-2">
<label for="show-only-active-chess" class="text-sm text-gray-700">仅显示已激活</label>
</div>
</div>
<div id="chess-weights-container" class="space-y-4">
{% for chess_id, weight in chess_weights.items() %}
<div class="chess-weight-item" data-chess-id="{{ chess_id }}" data-chess-name="{{ chess_id }}">
<div class="flex items-center justify-between mb-1">
<label class="text-gray-700">{{ chess_id }}</label>
<span class="chess-weight-value text-xs text-gray-700">{{ weight }}</span>
</div>
<div class="chess-weight-slider"></div>
</div>
{% endfor %}
</div>
</div>
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-center text-indigo-600 mb-8">云顶之弈阵容推荐器</h1>
<!-- 主要内容区 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 左侧设置区 -->
<div class="lg:col-span-1 bg-white rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4 text-indigo-700">基础设置</h2>
<div class="mb-6">
<label class="block text-gray-700 mb-2">人口数量</label>
<div class="flex items-center">
<input type="range" id="population" min="1" max="10" value="9"
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<span id="population-value" class="ml-2 text-gray-700">9</span>
</div>
</div>
<div class="mb-6">
<label class="block text-gray-700 mb-2">推荐结果数量</label>
<div class="flex items-center">
<input type="range" id="num-results" min="1" max="10" value="3"
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<span id="num-results-value" class="ml-2 text-gray-700">3</span>
</div>
</div>
<div class="flex mb-4">
<button id="open-weights-drawer" class="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700 mr-2">
打开权重设置
</button>
</div>
<!-- 必要羁绊设置 -->
<h2 class="text-xl font-semibold mb-4 mt-6 text-indigo-700">必要羁绊设置</h2>
<div class="mb-6">
<select id="required-synergy-select" class="w-full p-2 border border-gray-300 rounded mb-2">
<option value="">-- 选择羁绊 --</option>
{% for synergy in synergies %}
<option value="{{ synergy.jobId or synergy.raceId }}">{{ synergy.name }}</option>
{% endfor %}
</select>
<button id="add-required-synergy" class="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700">
添加必要羁绊
</button>
<div id="required-synergies-list" class="mt-2 space-y-2"></div>
</div>
<!-- 必选棋子设置 -->
<h2 class="text-xl font-semibold mb-4 mt-6 text-indigo-700">必选棋子设置</h2>
<div class="mb-6">
<select id="required-chess-select" class="w-full p-2 border border-gray-300 rounded mb-2">
<option value="">-- 选择棋子 --</option>
{% for c in chess %}
<option value="{{ c.chessId }}">{{ c.displayName }} ({{ c.price }}费)</option>
{% endfor %}
</select>
<button id="add-required-chess" class="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700">
添加必选棋子
</button>
<div id="required-chess-list" class="mt-2 space-y-2"></div>
</div>
<h2 class="text-xl font-semibold mb-4 mt-6 text-indigo-700">生成阵容</h2>
<button id="generate-btn" class="w-full bg-indigo-600 text-white px-4 py-3 rounded-lg hover:bg-indigo-700 transition-colors">
生成最佳阵容
</button>
</div>
<!-- 右侧结果展示区 -->
<div class="lg:col-span-1 bg-white rounded-lg shadow-md p-6 h-[800px] overflow-y-auto">
<h2 class="text-xl font-semibold mb-4 text-indigo-700">阵容推荐结果</h2>
<div id="loading" class="hidden">
<div class="flex justify-center items-center h-60">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
</div>
</div>
<div id="results-container" class="space-y-8"></div>
</div>
</div>
</div>
<!-- 结果模板 -->
<template id="team-result-template">
<div class="team-result p-4 bg-gray-50 rounded-lg">
<div class="flex justify-between items-center mb-2">
<h3 class="text-lg font-semibold text-gray-800">阵容 #<span class="team-number"></span></h3>
<span class="text-sm bg-indigo-100 text-indigo-800 px-2 py-1 rounded">
评分: <span class="team-score"></span>
</span>
</div>
<div class="mb-3">
<h4 class="text-md font-medium text-gray-700 mb-2">棋子列表</h4>
<div class="chess-list grid grid-cols-3 sm:grid-cols-3 md:grid-cols-3 gap-2"></div>
</div>
<div>
<h4 class="text-md font-medium text-gray-700 mb-2">激活羁绊</h4>
<div class="synergy-list space-y-2"></div>
</div>
</div>
</template>
<!-- JavaScript -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>

88
test_direct.py Normal file
View File

@ -0,0 +1,88 @@
"""
直接测试阵容推荐功能不通过HTTP请求
"""
from src.data_provider import DataQueryAPI
from src.scoring.scoring_system import TeamScorer
from src.recommendation import RecommendationEngine
import copy
def test_direct_recommendation():
"""直接测试阵容推荐功能"""
# 创建自定义配置
config = {
"base_weights": {
"synergy_level_weight": 1.5,
"synergy_count_weight": 0.8,
"chess_cost_weight": 0.3
},
"synergy_weights": {
"重装战士": 2.0,
"斗士": 1.8
},
"chess_weights": {
"盖伦": 2.0,
"赛娜": 2.5
},
"synergy_level_weights": {
"1": 1.0,
"2": 1.2,
"3": 1.5,
"4": 1.8,
"5": 2.0,
"6": 2.3,
"7": 2.6,
"8": 3.0,
"9": 3.3,
"10": 3.5
},
"cost_weights": {
"1": 1.0,
"2": 1.2,
"3": 1.5,
"4": 1.8,
"5": 2.0
}
}
# 初始化组件
data_api = DataQueryAPI()
scorer = TeamScorer(api=data_api, config_obj=copy.deepcopy(config))
engine = RecommendationEngine(api=data_api, scorer=scorer, config_obj=copy.deepcopy(config))
# 定义参数
population = 9
required_synergies = [] # 这里可以添加必选羁绊
required_chess = [] # 这里可以添加必选棋子
max_results = 3
# 生成阵容推荐
print("正在生成阵容推荐...")
teams = engine.recommend_team(
population=population,
required_synergies=required_synergies,
required_chess=required_chess,
max_results=max_results
)
# 打印结果
print(f"生成了 {len(teams)} 个推荐阵容")
for i, team in enumerate(teams):
print(f"\n阵容 #{i+1} (评分: {team.score:.2f})")
print("\n棋子列表:")
for chess in team.chess_list:
print(f" {chess.get('displayName')} ({chess.get('price')}费)")
print("\n激活的职业羁绊:")
for job in team.synergy_levels['job']:
print(f" {job['name']} (等级 {job['level']})")
print("\n激活的特质羁绊:")
for race in team.synergy_levels['race']:
print(f" {race['name']} (等级 {race['level']})")
print("\n" + "-"*50)
if __name__ == "__main__":
test_direct_recommendation()

66
test_recommendation.py Normal file
View File

@ -0,0 +1,66 @@
"""
测试阵容推荐功能
"""
import json
import requests
def test_recommendation():
"""
测试阵容推荐功能
"""
# 构建请求数据
data = {
"population": 9,
"num_results": 3,
"base_weights": {
"synergy_level_weight": 1.5,
"synergy_count_weight": 0.8,
"chess_cost_weight": 0.3
},
"synergy_weights": {
"重装战士": 2.0,
"斗士": 1.8
},
"chess_weights": {
"盖伦": 2.0,
"赛娜": 2.5
},
"required_synergies": [],
"required_chess": []
}
# 发送POST请求
url = "http://localhost:5000/api/recommend"
headers = {"Content-Type": "application/json"}
try:
response = requests.post(url, json=data, headers=headers)
# 检查响应状态码
if response.status_code == 200:
result = response.json()
print("请求成功!")
print(f"状态: {result['status']}")
print(f"推荐阵容数量: {len(result['results'])}")
# 打印第一个阵容的详细信息
if result['results']:
first_team = result['results'][0]
print(f"\n第一个推荐阵容评分: {first_team['score']}")
print("\n棋子列表:")
for chess in first_team['chess_list']:
print(f" {chess['name']} ({chess['cost']}费)")
print("\n激活羁绊:")
for synergy in first_team['active_synergies']:
print(f" {synergy['name']} (等级 {synergy['level']})")
else:
print(f"请求失败: {response.status_code}")
print(response.text)
except Exception as e:
print(f"发生错误: {str(e)}")
if __name__ == "__main__":
test_recommendation()