实现命令行参数解析,添加交互式命令行界面功能,并更新README文档以反映新功能和使用示例。

This commit is contained in:
hxuanyu 2025-04-02 10:19:04 +08:00
parent fd7392c6c9
commit 94535f8a3a
7 changed files with 1056 additions and 27 deletions

122
README.md
View File

@ -21,9 +21,10 @@
- 可配置的评分权重,支持自定义评分策略
- 多维度评估阵容强度,提供量化的阵容分析
4. **接口模块** (开发中)
4. **接口模块**
- 提供编程接口,方便集成到其他应用中
- 命令行界面,便于直接使用
- 交互式命令行界面,便于直接使用
- 支持自定义评分参数
## 环境与依赖
@ -76,6 +77,25 @@ python main.py recommend
- 指定必选棋子生成阵容
- 综合多种条件生成最优阵容
### 命令行界面
运行以下命令启动交互式命令行界面:
```bash
python main.py cli
```
命令行界面提供以下功能:
- 指定阵容人口和评分参数
- 交互式添加必选羁绊和等级
- 交互式添加必选棋子
- 展示详细的阵容推荐结果,包括棋子信息和激活的羁绊
您还可以使用命令行参数自定义配置:
```bash
python main.py cli --population 8 --results 5 --level-weight 1.2 --count-weight 0.7 --cost-weight 0.2
```
## 项目结构
```
@ -95,13 +115,17 @@ TFT-Strategist/
│ ├── scoring/ # 阵容评分模块
│ │ ├── __init__.py
│ │ └── scoring_system.py # 阵容评分系统
│ ├── interface/ # 接口模块 (开发中)
│ ├── interface/ # 接口模块
│ │ ├── __init__.py
│ │ ├── api.py # 编程接口
│ │ └── cli.py # 命令行界面
│ ├── __init__.py
│ ├── data_provider_demo.py # 数据提供模块演示
│ └── recommendation_demo.py # 阵容推荐模块演示
├── tests/ # 测试代码
│ ├── test_data_provider.py # 数据提供模块测试
│ └── test_recommendation.py # 阵容推荐模块测试 (开发中)
│ ├── test_recommendation.py # 阵容推荐模块测试
│ └── test_interface.py # 接口模块测试
├── main.py # 主程序入口
├── requirements.txt # 项目依赖
└── README.md # 项目文档
@ -242,13 +266,99 @@ print(f"阵容评分: {score}")
scorer.customize_scoring(synergy_level_weight=1.5)
```
## 接口模块详解
接口模块提供了便捷的方式与阵容推荐器进行交互,包括编程接口和命令行界面。
### 主要组件
1. **TFTStrategistAPI**: 编程接口类
- 提供完整的数据查询功能
- 提供阵容推荐和评分功能
- 支持自定义评分参数
2. **TFTCommandLine**: 命令行界面类
- 交互式用户界面
- 支持添加必选羁绊和棋子
- 详细展示阵容推荐结果
### 编程接口使用示例
```python
from src.interface import TFTStrategistAPI, get_api
# 使用单例模式获取API实例
api = get_api()
# 或者创建新的API实例
# api = TFTStrategistAPI(use_local_data=True)
# 获取游戏版本
version = api.get_version()
print(f"当前游戏版本: {version}")
# 自定义评分参数
api.customize_scoring(
synergy_level_weight=1.2,
synergy_count_weight=0.6,
chess_cost_weight=0.15
)
# 获取职业和特质信息
jobs = api.get_all_jobs()
races = api.get_all_races()
# 推荐阵容
required_synergies = [{'name': '超频战士', 'level': 3}]
required_chess = [{'name': '薇古丝'}]
teams = api.recommend_team(
population=8,
required_synergies=required_synergies,
required_chess=required_chess,
max_results=3
)
# 处理推荐结果
for i, team in enumerate(teams, 1):
print(f"\n推荐阵容 #{i} (评分: {team['score']:.2f})")
print(f"棋子: {[chess['displayName'] for chess in team['chess_list']]}")
```
### 命令行界面使用
直接通过main.py运行命令行界面
```bash
python main.py cli [参数]
```
可选参数:
- `--population`: 阵容人口数量默认为9
- `--results`: 返回结果数量默认为3
- `--level-weight`: 羁绊等级权重默认为1.0
- `--count-weight`: 羁绊数量权重默认为0.5
- `--cost-weight`: 棋子费用权重默认为0.1
在交互界面中,您可以:
1. 添加必选羁绊并指定等级
2. 添加必选棋子
3. 获取详细的阵容推荐结果
## 开发计划
- [x] 数据提供模块
- [x] 阵容推荐模块
- [x] 阵容评分模块
- [ ] 接口模块
- [ ] 图形用户界面
- [x] 接口模块
## 未来展望
- [ ] 图形用户界面(GUI)开发
- [ ] 基于历史数据的胜率分析
- [ ] 支持装备推荐
- [ ] 多语言支持
- [ ] 基于机器学习的个性化推荐
## 贡献指南

53
main.py
View File

@ -3,38 +3,53 @@
云顶之弈阵容推荐器 - 主程序
"""
import sys
import argparse
from src.data_provider_demo import main as data_provider_demo
from src.recommendation_demo import main as recommendation_demo
from src.interface.cli import main as cli_main
def main():
"""主函数"""
# 检查命令行参数
if len(sys.argv) > 1:
command = sys.argv[1]
if command == "data":
# 运行数据提供模块演示
data_provider_demo()
elif command == "recommend":
# 运行阵容推荐模块演示
recommendation_demo()
else:
print(f"未知命令: {command}")
print_usage()
# 创建参数解析器
parser = argparse.ArgumentParser(description='云顶之弈阵容推荐器')
subparsers = parser.add_subparsers(dest='command', help='命令')
# 数据提供模块命令
data_parser = subparsers.add_parser('data', help='运行数据提供模块演示')
# 阵容推荐模块命令
recommend_parser = subparsers.add_parser('recommend', help='运行阵容推荐模块演示')
# 命令行界面命令
cli_parser = subparsers.add_parser('cli', help='运行命令行界面')
# 解析参数
args = parser.parse_args()
# 根据命令执行相应的功能
if args.command == 'data':
# 运行数据提供模块演示
data_provider_demo()
elif args.command == 'recommend':
# 运行阵容推荐模块演示
recommendation_demo()
elif args.command == 'cli':
# 运行命令行界面
return cli_main()
else:
# 默认运行数据提供模块演示
print_usage()
print_usage(parser)
print("\n默认运行数据提供模块演示...\n")
data_provider_demo()
return 0
def print_usage():
def print_usage(parser):
"""打印使用帮助"""
print("使用方法: python main.py [命令]")
print("命令:")
print(" data 运行数据提供模块演示")
print(" recommend 运行阵容推荐模块演示")
parser.print_help()
if __name__ == "__main__":
main()
sys.exit(main())

View File

@ -1,3 +1,6 @@
"""
接口模块
接口模块 - 提供命令行和编程接口
"""
from src.interface.cli import TFTCommandLine
from src.interface.api import TFTStrategistAPI, get_api

275
src/interface/api.py Normal file
View File

@ -0,0 +1,275 @@
"""
编程接口模块 - 提供程序化调用阵容推荐器的API
此模块实现了一组API函数允许其他程序调用阵容推荐器的功能
1. 获取游戏数据
2. 查询羁绊和棋子信息
3. 生成阵容推荐
4. 自定义评分系统
"""
from typing import Dict, List, Any, Optional, Union, Tuple
import logging
from dataclasses import asdict
from src.data_provider import DataQueryAPI, DataLoader
from src.recommendation import RecommendationEngine
from src.scoring import TeamScorer, ScoringConfig
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("TFT-Strategist-API")
class TFTStrategistAPI:
"""云顶之弈阵容推荐API类"""
def __init__(self, use_local_data: bool = True):
"""
初始化API
Args:
use_local_data: 是否使用本地数据如果为False则从网络获取
"""
self.data_loader = DataLoader(use_local_if_exists=use_local_data)
self.api = DataQueryAPI(self.data_loader)
self.recommendation_engine = RecommendationEngine(api=self.api)
self.scorer = TeamScorer()
logger.info(f"TFT-Strategist-API 已初始化,游戏版本: {self.get_version()}")
def get_version(self) -> str:
"""
获取当前游戏版本
Returns:
str: 游戏版本
"""
return self.data_loader.get_latest_version() or "未知"
def customize_scoring(self, **kwargs) -> None:
"""
自定义评分系统参数
Args:
**kwargs: 评分参数可以包括
synergy_level_weight: 羁绊等级权重
synergy_count_weight: 羁绊数量权重
chess_cost_weight: 棋子费用权重
level_multipliers: 羁绊等级权重倍数字典
cost_multipliers: 棋子费用权重倍数字典
"""
self.scorer.customize_scoring(**kwargs)
logger.info(f"评分系统参数已更新: {kwargs}")
def get_scoring_config(self) -> Dict[str, Any]:
"""
获取当前评分配置
Returns:
Dict[str, Any]: 评分配置字典
"""
config = self.scorer.config
return {
'synergy_level_weight': config.synergy_level_weight,
'synergy_count_weight': config.synergy_count_weight,
'chess_cost_weight': config.chess_cost_weight,
'level_multipliers': config.level_multipliers,
'cost_multipliers': config.cost_multipliers
}
# 数据查询API
def get_all_jobs(self) -> List[Dict[str, Any]]:
"""
获取所有职业信息
Returns:
List[Dict[str, Any]]: 职业信息列表
"""
return self.api.get_all_jobs()
def get_all_races(self) -> List[Dict[str, Any]]:
"""
获取所有特质信息
Returns:
List[Dict[str, Any]]: 特质信息列表
"""
return self.api.get_all_races()
def get_all_chess(self) -> List[Dict[str, Any]]:
"""
获取所有棋子信息
Returns:
List[Dict[str, Any]]: 棋子信息列表
"""
return self.api.get_all_chess()
def get_job_by_name(self, name: str) -> Optional[Dict[str, Any]]:
"""
根据名称获取职业信息
Args:
name: 职业名称
Returns:
Optional[Dict[str, Any]]: 职业信息如果未找到则返回None
"""
return self.api.get_job_by_name(name)
def get_race_by_name(self, name: str) -> Optional[Dict[str, Any]]:
"""
根据名称获取特质信息
Args:
name: 特质名称
Returns:
Optional[Dict[str, Any]]: 特质信息如果未找到则返回None
"""
return self.api.get_race_by_name(name)
def get_chess_by_name(self, name: str) -> Optional[Dict[str, Any]]:
"""
根据名称获取棋子信息
Args:
name: 棋子名称
Returns:
Optional[Dict[str, Any]]: 棋子信息如果未找到则返回None
"""
return self.api.get_chess_by_name(name)
def get_chess_by_job(self, job_id: str) -> List[Dict[str, Any]]:
"""
获取指定职业的所有棋子
Args:
job_id: 职业ID
Returns:
List[Dict[str, Any]]: 棋子信息列表
"""
return self.api.get_chess_by_job(job_id)
def get_chess_by_race(self, race_id: str) -> List[Dict[str, Any]]:
"""
获取指定特质的所有棋子
Args:
race_id: 特质ID
Returns:
List[Dict[str, Any]]: 棋子信息列表
"""
return self.api.get_chess_by_race(race_id)
def get_synergies_of_chess(self, chess_id: str) -> List[Dict[str, Any]]:
"""
获取棋子的所有羁绊信息
Args:
chess_id: 棋子ID
Returns:
List[Dict[str, Any]]: 羁绊信息列表
"""
return self.api.get_synergies_of_chess(chess_id)
def get_synergy_levels(self, synergy_id: str) -> Dict[str, str]:
"""
获取羁绊的等级信息
Args:
synergy_id: 羁绊ID
Returns:
Dict[str, str]: 等级信息字典键为等级值为效果描述
"""
return self.api.get_synergy_levels(synergy_id)
# 阵容推荐API
def recommend_team(
self,
population: int = 9,
required_synergies: Optional[List[Dict[str, Any]]] = None,
required_chess: Optional[List[Dict[str, Any]]] = None,
max_results: int = 5
) -> List[Dict[str, Any]]:
"""
推荐阵容
Args:
population: 阵容人口数默认为9
required_synergies: 必须包含的羁绊列表每个羁绊为一个字典包含name和level
required_chess: 必须包含的棋子列表每个棋子为一个字典包含name
max_results: 最多返回的推荐阵容数量
Returns:
List[Dict[str, Any]]: 推荐的阵容列表每个阵容为一个字典
"""
# 参数处理
required_synergies = required_synergies or []
required_chess = required_chess or []
# 生成阵容推荐
teams = self.recommendation_engine.recommend_team(
population=population,
required_synergies=required_synergies,
required_chess=required_chess,
max_results=max_results
)
# 转换为字典
return [team.to_dict() for team in teams]
def score_team(self, team_dict: Dict[str, Any]) -> float:
"""
计算阵容评分
Args:
team_dict: 阵容字典包含chess_list等字段
Returns:
float: 阵容评分
"""
from src.recommendation.recommendation_engine import TeamComposition
# 创建TeamComposition对象
team = TeamComposition()
# 添加棋子
for chess in team_dict.get('chess_list', []):
team.add_chess(chess)
# 计算羁绊
team.calculate_synergies(self.api)
# 计算评分
score = self.scorer.score_team(team)
return score
# 全局API实例
_api_instance = None
def get_api(use_local_data: bool = True) -> TFTStrategistAPI:
"""
获取API实例单例模式
Args:
use_local_data: 是否使用本地数据
Returns:
TFTStrategistAPI: API实例
"""
global _api_instance
if _api_instance is None:
_api_instance = TFTStrategistAPI(use_local_data=use_local_data)
return _api_instance

196
src/interface/cli.py Normal file
View File

@ -0,0 +1,196 @@
"""
命令行接口模块 - 提供用户与阵容推荐器交互的功能
此模块实现了一个命令行界面允许用户以交互式方式
1. 指定阵容人口
2. 添加必选羁绊和等级
3. 添加必选棋子
4. 获取阵容推荐结果并展示
"""
import argparse
import os
import sys
from typing import Dict, List, Any, Optional, Tuple
import logging
from src.data_provider import DataQueryAPI
from src.recommendation import RecommendationEngine
from src.scoring import TeamScorer, ScoringConfig
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("TFT-Strategist-CLI")
class TFTCommandLine:
"""云顶之弈阵容推荐命令行接口"""
def __init__(self):
"""初始化命令行接口"""
self.api = DataQueryAPI()
self.recommendation_engine = RecommendationEngine(api=self.api)
self.scorer = TeamScorer()
def run(self):
"""运行命令行界面"""
parser = argparse.ArgumentParser(description='云顶之弈阵容推荐器命令行界面', add_help=False)
parser.add_argument('--population', type=int, default=9, help='阵容人口数量默认为9')
parser.add_argument('--results', type=int, default=3, help='返回结果数量默认为3')
parser.add_argument('--level-weight', type=float, default=1.0, help='羁绊等级权重默认为1.0')
parser.add_argument('--count-weight', type=float, default=0.5, help='羁绊数量权重默认为0.5')
parser.add_argument('--cost-weight', type=float, default=0.1, help='棋子费用权重默认为0.1')
# 解析参数,但忽略未知的参数,以便与主程序的参数解析兼容
args, _ = parser.parse_known_args()
# 自定义评分权重
self.scorer.customize_scoring(
synergy_level_weight=args.level_weight,
synergy_count_weight=args.count_weight,
chess_cost_weight=args.cost_weight
)
# 初始化阵容参数
population = args.population
max_results = args.results
required_synergies = []
required_chess = []
print("\n==== 云顶之弈阵容推荐器 ====\n")
version = self.api.data_loader.get_latest_version()
print(f"当前游戏版本: {version}")
print(f"阵容人口: {population}")
# 交互式添加必选羁绊
print("\n-- 添加必选羁绊 --")
while True:
synergy_name = input("请输入羁绊名称(直接回车跳过): ").strip()
if not synergy_name:
break
synergy = self.api.get_synergy_by_name(synergy_name)
if not synergy:
print(f"未找到羁绊: {synergy_name}")
continue
synergy_id = synergy.get('jobId') or synergy.get('raceId')
levels = self.api.get_synergy_levels(synergy_id)
print(f"羁绊 {synergy_name} 的可用等级:")
for level_str, effect in levels.items():
print(f" {level_str}: {effect}")
while True:
level_input = input("请输入所需等级(直接回车使用最低等级): ").strip()
if not level_input:
min_level = min(int(l) for l in levels.keys())
level = min_level
break
try:
level = int(level_input)
if str(level) in levels:
break
else:
print(f"无效的等级,请选择: {', '.join(levels.keys())}")
except ValueError:
print("请输入数字")
required_synergies.append({
'name': synergy_name,
'level': level
})
print(f"已添加羁绊: {synergy_name}, 等级: {level}")
# 交互式添加必选棋子
print("\n-- 添加必选棋子 --")
while True:
chess_name = input("请输入棋子名称(直接回车跳过): ").strip()
if not chess_name:
break
chess = self.api.get_chess_by_name(chess_name)
if not chess:
print(f"未找到棋子: {chess_name}")
continue
required_chess.append({
'name': chess_name
})
print(f"已添加棋子: {chess_name}")
# 生成阵容推荐
print("\n正在生成阵容推荐...")
teams = self.recommendation_engine.recommend_team(
population=population,
required_synergies=required_synergies,
required_chess=required_chess,
max_results=max_results
)
# 显示推荐结果
print("\n==== 阵容推荐结果 ====")
for i, team in enumerate(teams, 1):
print(f"\n*** 推荐阵容 #{i} (评分: {team.score:.2f}) ***")
# 显示棋子列表
print("棋子:")
for chess in team.chess_list:
# 获取棋子的羁绊
job_names = []
race_names = []
for job_id in chess.get('jobIds', '').split(','):
if job_id:
job = self.api.get_job_by_id(job_id)
if job:
job_names.append(job['name'])
for race_id in chess.get('raceIds', '').split(','):
if race_id:
race = self.api.get_race_by_id(race_id)
if race:
race_names.append(race['name'])
job_str = ', '.join(job_names) if job_names else ''
race_str = ', '.join(race_names) if race_names else ''
print(f" - {chess['displayName']} ({chess['price']}金币) | 职业: {job_str} | 特质: {race_str}")
# 显示激活的羁绊
print("\n激活的羁绊:")
# 职业
if team.synergy_levels['job']:
print("职业:")
for job_level in sorted(team.synergy_levels['job'], key=lambda x: x['name']):
print(f" - {job_level['name']} ({job_level['count']}/{job_level['level']}): {job_level['effect']}")
# 特质
if team.synergy_levels['race']:
print("特质:")
for race_level in sorted(team.synergy_levels['race'], key=lambda x: x['name']):
print(f" - {race_level['name']} ({race_level['count']}/{race_level['level']}): {race_level['effect']}")
print("\n------------------------")
def main():
"""主函数"""
cli = TFTCommandLine()
try:
cli.run()
except KeyboardInterrupt:
print("\n程序已退出")
except Exception as e:
logger.error(f"发生错误: {e}", exc_info=True)
print(f"\n程序发生错误: {e}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

207
tests/test_interface.py Normal file
View File

@ -0,0 +1,207 @@
"""
测试接口模块
"""
import os
import sys
import unittest
from unittest import mock
from io import StringIO
# 添加项目根目录到Python路径
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from src.interface.api import TFTStrategistAPI
from src.interface.cli import TFTCommandLine
from src.recommendation.recommendation_engine import TeamComposition
from src.data_provider import DataLoader, DataQueryAPI
class TestTFTStrategistAPI(unittest.TestCase):
"""测试TFTStrategistAPI类"""
def setUp(self):
"""测试前准备"""
# 模拟DataLoader
self.mock_loader = mock.Mock()
self.mock_loader.load_all_data.return_value = True
# 模拟加载的数据
self.job_data = {
"version": "15.7",
"data": [
{
"jobId": "10157",
"name": "重装战士",
"level": {
"2": "16%最大生命值",
"4": "32%最大生命值",
}
}
]
}
self.race_data = {
"version": "15.7",
"data": [
{
"raceId": "10154",
"name": "街头恶魔",
"level": {
"3": "+6% 生命值",
"5": "+10% 生命值",
}
}
]
}
self.chess_data = {
"version": "15.7",
"data": [
{
"chessId": "10275",
"displayName": "布兰德",
"raceIds": "10154",
"jobIds": "10172",
"price": "4"
},
{
"chessId": "10276",
"displayName": "阿利斯塔",
"raceIds": "",
"jobIds": "10157",
"price": "3"
}
]
}
# 设置模拟数据返回
self.mock_loader.get_data.side_effect = lambda data_type: {
'job': self.job_data,
'race': self.race_data,
'chess': self.chess_data
}.get(data_type)
# 模拟DataQueryAPI
self.mock_api = mock.Mock()
self.mock_api.get_version.return_value = "15.7"
self.mock_api.get_all_jobs.return_value = self.job_data["data"]
self.mock_api.get_all_races.return_value = self.race_data["data"]
self.mock_api.get_all_chess.return_value = self.chess_data["data"]
self.mock_api.load_all_data.return_value = True
# 模拟RecommendationEngine
self.mock_recommendation_engine = mock.Mock()
# 创建API实例并注入模拟对象
with mock.patch('src.interface.api.DataQueryAPI', return_value=self.mock_api):
with mock.patch('src.interface.api.RecommendationEngine', return_value=self.mock_recommendation_engine):
self.api = TFTStrategistAPI(use_local_data=True)
def test_get_version(self):
"""测试获取版本号"""
version = self.api.get_version()
self.assertEqual(version, "15.7")
# 由于在初始化和测试中都会调用get_version所以不再断言调用次数
def test_customize_scoring(self):
"""测试自定义评分系统"""
# 捕获调用customize_scoring时的参数
with mock.patch.object(self.api.scorer, 'customize_scoring') as mock_customize:
self.api.customize_scoring(synergy_level_weight=2.0)
mock_customize.assert_called_once_with(synergy_level_weight=2.0)
def test_get_scoring_config(self):
"""测试获取评分配置"""
config = self.api.get_scoring_config()
self.assertIn('synergy_level_weight', config)
self.assertIn('synergy_count_weight', config)
self.assertIn('chess_cost_weight', config)
def test_recommend_team(self):
"""测试推荐阵容"""
# 创建模拟的TeamComposition
team = TeamComposition()
team.chess_list = [{"displayName": "布兰德", "price": "4"}]
team.score = 10.5
# 设置recommend_team的返回值
self.mock_recommendation_engine.recommend_team.return_value = [team]
# 调用API
teams = self.api.recommend_team(population=8)
# 验证结果
self.assertEqual(len(teams), 1)
self.assertEqual(teams[0]['score'], 10.5)
self.assertEqual(len(teams[0]['chess_list']), 1)
self.assertEqual(teams[0]['chess_list'][0]['displayName'], "布兰德")
# 验证调用参数
self.mock_recommendation_engine.recommend_team.assert_called_once_with(
population=8,
required_synergies=[],
required_chess=[],
max_results=5
)
def test_score_team(self):
"""测试计算阵容评分"""
# 设置score_team的返回值
self.api.scorer.score_team = mock.Mock(return_value=15.5)
# 创建阵容数据
team_dict = {
'chess_list': [{"displayName": "布兰德", "price": "4"}]
}
# 调用API
score = self.api.score_team(team_dict)
# 验证结果
self.assertEqual(score, 15.5)
class TestTFTCommandLine(unittest.TestCase):
"""测试TFTCommandLine类"""
@mock.patch('src.interface.cli.input', side_effect=['', ''])
@mock.patch('sys.stdout', new_callable=StringIO)
def test_run_without_input(self, mock_stdout, mock_input):
"""测试不输入羁绊和棋子时的运行"""
# 模拟RecommendationEngine返回空阵容列表
mock_engine = mock.Mock()
mock_engine.recommend_team.return_value = []
# 模拟API
mock_api = mock.Mock()
mock_api.get_version.return_value = "15.7"
# 使用模拟对象创建CLI实例
with mock.patch('src.interface.cli.DataQueryAPI', return_value=mock_api):
with mock.patch('src.interface.cli.RecommendationEngine', return_value=mock_engine):
cli = TFTCommandLine()
# 捕获参数解析错误
with mock.patch('argparse.ArgumentParser.parse_args',
return_value=mock.MagicMock(
population=9, results=3,
level_weight=1.0, count_weight=0.5, cost_weight=0.1)):
cli.run()
# 验证输出包含版本信息
output = mock_stdout.getvalue()
self.assertIn("云顶之弈阵容推荐器", output)
self.assertIn("15.7", output) # 版本号
self.assertIn("阵容人口: 9", output)
# 验证推荐调用
mock_engine.recommend_team.assert_called_once_with(
population=9,
required_synergies=[],
required_chess=[],
max_results=3
)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,223 @@
"""
测试阵容推荐模块
"""
import os
import sys
import unittest
from unittest import mock
# 添加项目根目录到Python路径
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from src.recommendation.recommendation_engine import RecommendationEngine, TeamComposition
from src.data_provider import DataQueryAPI
from src.scoring import TeamScorer
class TestTeamComposition(unittest.TestCase):
"""测试TeamComposition类"""
def test_add_chess(self):
"""测试添加棋子"""
team = TeamComposition()
chess = {"displayName": "布兰德", "price": "4", "jobIds": "10172", "raceIds": "10154"}
# 添加棋子
team.add_chess(chess)
# 验证结果
self.assertEqual(len(team.chess_list), 1)
self.assertEqual(team.chess_list[0], chess)
self.assertEqual(team.size, 1)
self.assertEqual(team.total_cost, 4)
# 重复添加同一个棋子,应该不会改变
team.add_chess(chess)
self.assertEqual(len(team.chess_list), 1)
def test_calculate_synergies(self):
"""测试计算羁绊"""
team = TeamComposition()
# 添加棋子
team.add_chess({
"displayName": "布兰德",
"price": "4",
"jobIds": "10172",
"raceIds": "10154"
})
team.add_chess({
"displayName": "阿利斯塔",
"price": "3",
"jobIds": "10157",
"raceIds": ""
})
team.add_chess({
"displayName": "其他棋子",
"price": "2",
"jobIds": "10172",
"raceIds": "10154"
})
# 模拟DataQueryAPI
mock_api = mock.Mock()
# 设置get_synergy_by_id的返回值
def get_synergy_by_id(synergy_id):
synergies = {
"10172": {"jobId": "10172", "name": "高级工程师"},
"10157": {"jobId": "10157", "name": "重装战士"},
"10154": {"raceId": "10154", "name": "街头恶魔"}
}
return synergies.get(synergy_id)
mock_api.get_synergy_by_id.side_effect = get_synergy_by_id
# 设置get_synergy_levels的返回值
def get_synergy_levels(synergy_id):
levels = {
"10172": {"2": "效果1", "4": "效果2"},
"10157": {"2": "效果1", "4": "效果2"},
"10154": {"3": "效果1", "5": "效果2"}
}
return levels.get(synergy_id)
mock_api.get_synergy_levels.side_effect = get_synergy_levels
# 计算羁绊
team.calculate_synergies(mock_api)
# 验证结果
self.assertEqual(team.synergy_counts.get("10172"), 2) # 高级工程师有2个
self.assertEqual(team.synergy_counts.get("10157"), 1) # 重装战士有1个
self.assertEqual(team.synergy_counts.get("10154"), 2) # 街头恶魔有2个
# 验证激活的羁绊等级
job_levels = team.synergy_levels['job']
race_levels = team.synergy_levels['race']
# 应该有高级工程师(2)和重装战士(0)的羁绊
self.assertEqual(len(job_levels), 1) # 只有高级工程师达到了激活等级
self.assertEqual(job_levels[0]['name'], "高级工程师")
self.assertEqual(job_levels[0]['level'], 2)
# 重装战士未激活街头恶魔未激活等级3
self.assertEqual(len(race_levels), 0)
def test_to_dict(self):
"""测试转换为字典"""
team = TeamComposition()
team.chess_list = [{"displayName": "布兰德", "price": "4"}]
team.synergy_counts = {"10172": 1}
team.synergy_levels = {'job': [{'name': '高级工程师', 'level': 2}], 'race': []}
team.total_cost = 4
team.score = 10.5
# 转换为字典
result = team.to_dict()
# 验证结果
self.assertEqual(result['chess_list'], team.chess_list)
self.assertEqual(result['synergy_counts'], team.synergy_counts)
self.assertEqual(result['synergy_levels'], team.synergy_levels)
self.assertEqual(result['total_cost'], team.total_cost)
self.assertEqual(result['size'], team.size)
self.assertEqual(result['score'], team.score)
class TestRecommendationEngine(unittest.TestCase):
"""测试RecommendationEngine类"""
def setUp(self):
"""测试前准备"""
# 模拟DataQueryAPI
self.mock_api = mock.Mock()
# 模拟TeamScorer
self.mock_scorer = mock.Mock()
# 创建RecommendationEngine实例
self.engine = RecommendationEngine(api=self.mock_api, scorer=self.mock_scorer)
def test_recommend_team_basic(self):
"""测试基本的阵容推荐功能"""
# 创建模拟的TeamComposition
team1 = TeamComposition()
team1.chess_list = [{"displayName": "布兰德", "price": "4"}]
team1.score = 10.5
team2 = TeamComposition()
team2.chess_list = [{"displayName": "阿利斯塔", "price": "3"}]
team2.score = 8.2
# 模拟_generate_candidate_teams方法
mock_generate = mock.Mock(return_value=[team1, team2])
self.engine._generate_candidate_teams = mock_generate
# 模拟calculate_synergies方法
def mock_calculate_synergies(api):
pass
team1.calculate_synergies = mock_calculate_synergies
team2.calculate_synergies = mock_calculate_synergies
# 模拟score_team方法
self.mock_scorer.score_team.side_effect = lambda team: team.score
# 调用recommend_team
result = self.engine.recommend_team(population=8, max_results=1)
# 验证结果
self.assertEqual(len(result), 1)
self.assertEqual(result[0].score, 10.5) # 应该返回得分最高的阵容
# 验证调用
mock_generate.assert_called_once()
def test_recommend_team_with_required_chess(self):
"""测试指定必选棋子的阵容推荐"""
# 设置get_chess_by_name的返回值
self.mock_api.get_chess_by_name.return_value = {
"displayName": "布兰德",
"price": "4"
}
# 模拟_generate_candidate_teams方法
self.engine._generate_candidate_teams = mock.Mock(return_value=[])
# 调用recommend_team
required_chess = [{"name": "布兰德"}]
self.engine.recommend_team(required_chess=required_chess)
# 验证get_chess_by_name被调用
self.mock_api.get_chess_by_name.assert_called_once_with("布兰德")
def test_recommend_team_with_required_synergies(self):
"""测试指定必选羁绊的阵容推荐"""
# 设置get_synergy_by_name的返回值
self.mock_api.get_synergy_by_name.return_value = {
"jobId": "10172",
"name": "高级工程师"
}
# 设置get_synergy_levels的返回值
self.mock_api.get_synergy_levels.return_value = {
"2": "效果1",
"4": "效果2"
}
# 模拟_generate_candidate_teams方法
self.engine._generate_candidate_teams = mock.Mock(return_value=[])
# 调用recommend_team
required_synergies = [{"name": "高级工程师", "level": 2}]
self.engine.recommend_team(required_synergies=required_synergies)
# 验证get_synergy_by_name被调用
self.mock_api.get_synergy_by_name.assert_called_once_with("高级工程师")
if __name__ == '__main__':
unittest.main()