diff --git a/README.md b/README.md index ba0bdf4..eb99f49 100644 --- a/README.md +++ b/README.md @@ -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)开发 +- [ ] 基于历史数据的胜率分析 +- [ ] 支持装备推荐 +- [ ] 多语言支持 +- [ ] 基于机器学习的个性化推荐 ## 贡献指南 diff --git a/main.py b/main.py index 3125c74..42fc20a 100644 --- a/main.py +++ b/main.py @@ -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() \ No newline at end of file + sys.exit(main()) \ No newline at end of file diff --git a/src/interface/__init__.py b/src/interface/__init__.py index b16cd73..fef3bfc 100644 --- a/src/interface/__init__.py +++ b/src/interface/__init__.py @@ -1,3 +1,6 @@ """ -接口模块 -""" \ No newline at end of file +接口模块 - 提供命令行和编程接口 +""" + +from src.interface.cli import TFTCommandLine +from src.interface.api import TFTStrategistAPI, get_api \ No newline at end of file diff --git a/src/interface/api.py b/src/interface/api.py new file mode 100644 index 0000000..3f81758 --- /dev/null +++ b/src/interface/api.py @@ -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 \ No newline at end of file diff --git a/src/interface/cli.py b/src/interface/cli.py new file mode 100644 index 0000000..3a61545 --- /dev/null +++ b/src/interface/cli.py @@ -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()) \ No newline at end of file diff --git a/tests/test_interface.py b/tests/test_interface.py new file mode 100644 index 0000000..2022845 --- /dev/null +++ b/tests/test_interface.py @@ -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() \ No newline at end of file diff --git a/tests/test_recommendation.py b/tests/test_recommendation.py new file mode 100644 index 0000000..3c0e418 --- /dev/null +++ b/tests/test_recommendation.py @@ -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() \ No newline at end of file