diff --git a/data/weights_config.yaml b/data/weights_config.yaml index 50492b2..8e5c660 100644 --- a/data/weights_config.yaml +++ b/data/weights_config.yaml @@ -1,121 +1,81 @@ -# 云顶之弈阵容评分权重配置文件 - -# 基础权重配置 base_weights: - synergy_level_weight: 1.0 - synergy_count_weight: 0.5 chess_cost_weight: 0.1 - -# 羁绊权重配置(值越大,该羁绊在评分中的权重越高) -synergy_weights: - # 职业羁绊 - 人造人: 1.4 - 召唤物: 0.7 - 堡垒卫士: 1.4 - 强袭射手: 1.4 - 战略分析师: 1.1 - 斗士: 1.5 - 杀手: 1.4 - 裁决使: 1.4 - 超频战士: 1.6 - 迅捷射手: 1.5 - 重装战士: 1.5 - 高级工程师: 1.5 - - # 特质羁绊 - 圣灵使者: 1.5 - 幻灵战队: 1.4 - 弑魂者: 1.6 - 战地机甲: 1.2 - 执事: 1.3 - 源计划: 1.5 - 病毒魔人: 1.0 - 福牛守护者: 1.2 - 网络之神: 1.6 - 街头恶魔: 1.4 - 赛博老大: 1.3 - 辛迪加: 1.3 - 魔装机神: 1.1 - 鳄霸: 1.6 - -# 棋子费用等级权重(费用越高权重越大) + synergy_count_weight: 0.5 + synergy_level_weight: 1.0 +chess_weights: + R-080T: 0 + T-43X: 0 + T-URR37: 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 + 德莱厄斯: 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 + 韦鲁斯: 1 + 魔像: 0 cost_weights: '1': 1.0 '2': 1.2 '3': 1.5 '4': 1.8 '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: '1': 1.0 + '10': 3.5 '2': 1.2 '3': 1.5 '4': 1.8 @@ -124,4 +84,30 @@ synergy_level_weights: '7': 2.6 '8': 3.0 '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 diff --git a/main.py b/main.py index faba969..86361fa 100644 --- a/main.py +++ b/main.py @@ -13,6 +13,7 @@ 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( @@ -33,6 +34,7 @@ def main(): # 阵容推荐模块演示 recommend_parser = subparsers.add_parser("recommend", help="阵容推荐模块演示") + recommend_parser.add_argument("--config", type=str, help="指定配置文件路径") # 命令行界面 cli_parser = subparsers.add_parser("cli", help="交互式命令行界面") @@ -41,16 +43,26 @@ def main(): 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="指定配置文件路径") - # Web界面 (新增) + # 权重配置管理(新增) + 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() @@ -64,7 +76,7 @@ def main(): elif args.command == "recommend": logger.info("启动阵容推荐模块演示") # 调用阵容推荐模块演示函数 - recommendation_demo() + recommendation_demo(config_path=getattr(args, 'config', None)) elif args.command == "cli": logger.info("启动交互式命令行界面") @@ -74,17 +86,70 @@ def main(): results=args.results, level_weight=args.level_weight, count_weight=args.count_weight, - cost_weight=args.cost_weight + cost_weight=args.cost_weight, + config_path=getattr(args, 'config', None) ) elif args.command == "scoring": logger.info("启动评分模块测试") - if args.config: - # 如果指定了配置文件路径,使用该路径进行测试 - test_scoring(config_path=args.config) + 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: - # 否则使用默认配置文件 - test_scoring() + config_parser.print_help() elif args.command == "web": logger.info("启动Web界面") @@ -92,7 +157,8 @@ def main(): run_server( host=args.host, port=args.port, - dev_mode=args.dev + dev_mode=args.dev, + config_path=getattr(args, 'config', None) ) else: diff --git a/src/config/__init__.py b/src/config/__init__.py new file mode 100644 index 0000000..e1304d7 --- /dev/null +++ b/src/config/__init__.py @@ -0,0 +1,11 @@ +""" +全局配置模块 - 提供整个应用的配置管理 + +此模块包含应用程序的全局配置管理功能,包括: +1. 权重配置的加载和管理 +2. 全局参数的设置和获取 +""" + +from src.config.weights_config import WeightsConfig, get_global_weights_config + +__all__ = ["WeightsConfig", "get_global_weights_config"] \ No newline at end of file diff --git a/src/config/weights_config.py b/src/config/weights_config.py new file mode 100644 index 0000000..78f549e --- /dev/null +++ b/src/config/weights_config.py @@ -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 \ No newline at end of file diff --git a/src/interface/cli.py b/src/interface/cli.py index 260bf02..2917794 100644 --- a/src/interface/cli.py +++ b/src/interface/cli.py @@ -16,6 +16,7 @@ import logging from src.data_provider import DataQueryAPI from src.recommendation import RecommendationEngine from src.scoring import TeamScorer, ScoringConfig +from src.config import get_global_weights_config # 配置日志 logging.basicConfig( @@ -28,11 +29,20 @@ logger = logging.getLogger("TFT-Strategist-CLI") 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.recommendation_engine = RecommendationEngine(api=self.api) - self.scorer = TeamScorer() + self.scorer = TeamScorer(config_path=config_path) + self.recommendation_engine = RecommendationEngine(api=self.api, scorer=self.scorer, config_path=config_path) def run(self, population=None, results=None, level_weight=None, count_weight=None, cost_weight=None): """ @@ -62,12 +72,16 @@ class TFTCommandLine: count_weight = count_weight or args.count_weight cost_weight = cost_weight or args.cost_weight - # 自定义评分权重 - self.scorer.customize_scoring( - synergy_level_weight=level_weight, - synergy_count_weight=count_weight, - chess_cost_weight=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() # 初始化阵容参数 required_synergies = [] @@ -78,6 +92,13 @@ class TFTCommandLine: print(f"当前游戏版本: {version}") 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-- 添加必选羁绊 --") while True: @@ -113,6 +134,22 @@ class TFTCommandLine: except ValueError: 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({ 'name': synergy_name, 'level': level @@ -130,6 +167,22 @@ class TFTCommandLine: if not chess: print(f"未找到棋子: {chess_name}") 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({ 'name': chess_name @@ -192,7 +245,7 @@ class TFTCommandLine: print("\n------------------------") -def main(population=None, results=None, level_weight=None, count_weight=None, cost_weight=None): +def main(population=None, results=None, level_weight=None, count_weight=None, cost_weight=None, config_path=None): """ 主函数 @@ -202,26 +255,18 @@ def main(population=None, results=None, level_weight=None, count_weight=None, co level_weight (float, optional): 羁绊等级权重 count_weight (float, optional): 羁绊数量权重 cost_weight (float, optional): 棋子费用权重 - - Returns: - int: 退出码 + config_path (str, optional): 配置文件路径 """ - cli = TFTCommandLine() try: - cli.run( - population=population, - results=results, - level_weight=level_weight, - count_weight=count_weight, - cost_weight=cost_weight - ) + cli = TFTCommandLine(config_path=config_path) + cli.run(population, results, level_weight, count_weight, cost_weight) + return 0 except KeyboardInterrupt: - print("\n程序已退出") - except Exception as e: - logger.error(f"发生错误: {e}", exc_info=True) - print(f"\n程序发生错误: {e}") + print("\n操作被用户中断") + return 1 + except Exception as e: + logger.error(f"发生错误: {str(e)}") return 1 - return 0 if __name__ == "__main__": diff --git a/src/recommendation/recommendation_engine.py b/src/recommendation/recommendation_engine.py index 1b405aa..2db0381 100644 --- a/src/recommendation/recommendation_engine.py +++ b/src/recommendation/recommendation_engine.py @@ -12,6 +12,7 @@ import itertools from dataclasses import dataclass, field from src.data_provider import DataQueryAPI from src.scoring.scoring_system import TeamScorer +from src.config import get_global_weights_config # 配置日志 logging.basicConfig( @@ -109,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: api: 数据查询API实例,如果为None则创建一个新的实例 scorer: 阵容评分系统实例,如果为None则创建一个新的实例 + config_path: 配置文件路径,用于全局权重配置 + config_obj: 配置对象,优先于config_path使用 """ 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( self, @@ -198,27 +209,37 @@ class RecommendationEngine: break 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': synergy, 'chess_list': chess_list, - 'target_level': target_level + 'target_level': target_level, + 'weight': weight # 添加权重信息 }) - # 3. 生成候选阵容 - candidate_teams = self._generate_candidate_teams( - base_team, synergy_chess_sets, population - ) + # 3. 根据已选棋子和羁绊要求生成候选阵容 + candidate_teams = self._generate_candidate_teams(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: team.calculate_synergies(self.api) 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] def _generate_candidate_teams( @@ -228,107 +249,105 @@ class RecommendationEngine: population: int ) -> List[TeamComposition]: """ - 生成候选阵容 + 根据羁绊要求生成候选阵容 Args: base_team: 基础阵容,包含必选棋子 - synergy_chess_sets: 各个必选羁绊的棋子集合 + synergy_chess_sets: 羁绊需要的棋子集合列表 population: 人口限制 Returns: List[TeamComposition]: 候选阵容列表 """ - # 已经有基础棋子 - remaining_slots = population - base_team.size - - # 如果没有必选羁绊,则直接从所有棋子中选择 + # 如果没有羁绊要求,则直接填充最佳棋子 if not synergy_chess_sets: + remaining_slots = population - base_team.size 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 = [] - # 为每个羁绊选择合适的棋子 - 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: - synergy = synergy_set['synergy'] chess_list = synergy_set['chess_list'] target_level = synergy_set['target_level'] - # 过滤掉已经在基础阵容中的棋子 - available_chess = [ - chess for chess in chess_list - if chess not in base_team.chess_list - ] + # 计算需要添加的棋子数量 + already_in_base = sum(1 for chess in base_team.chess_list + if any(job_id in chess.get('jobIds', '').split(',') + 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')])) - # 计算还需要多少个该羁绊的棋子 - synergy_id = synergy.get('jobId') or synergy.get('raceId') - - # 统计基础阵容中有多少个该羁绊的棋子 - 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: + needed_count = max(0, target_level - already_in_base) + if needed_count <= 0: + # 如果基础阵容已经满足该羁绊要求,则跳过 continue + + # 从该羁绊的棋子中选择还未在基础阵容中的 + available_chess = [chess for chess in chess_list + if chess.get('displayName') not in base_chess_names] - # 如果可用棋子不足,则尽可能多选 + # 如果可用棋子不足以满足羁绊要求,则返回空列表 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)) - synergy_combinations.append(combinations) + # 所有可能的棋子组合 + chess_combinations = list(itertools.combinations(available_chess, needed_count)) + + # 将权重因素纳入棋子选择决策 + 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: - return self._fill_team_with_best_chess(base_team, remaining_slots) + # 如果某个羁绊无法满足,则返回空列表 + if not all(min_chess_options): + return [] - # 生成所有羁绊组合的笛卡尔积 - for combo_product in itertools.product(*synergy_combinations): - # 扁平化组合成一个列表 - flat_combo = [] - for combo in combo_product: - 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() + # 2. 尝试组合不同羁绊的棋子,生成可行阵容 + # 为每个羁绊选择一种棋子组合方式 + for chess_selection in itertools.product(*min_chess_options): + # 创建新的候选阵容 + candidate = TeamComposition() + # 添加基础阵容的棋子 for chess in base_team.chess_list: - new_team.add_chess(chess) + candidate.add_chess(chess) - # 添加羁绊棋子 - for chess in unique_combo: - new_team.add_chess(chess) + # 添加各羁绊选中的棋子 + all_selected_chess = set() + 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: - new_remaining_slots = population - new_team.size - filled_teams = self._fill_team_with_best_chess(new_team, new_remaining_slots) - if filled_teams: - candidate_teams.extend(filled_teams) - else: - candidate_teams.append(new_team) - - # 如果没有生成候选阵容,直接填充最好的棋子 - if not candidate_teams: - return self._fill_team_with_best_chess(base_team, remaining_slots) + # 检查是否超出人口限制 + if candidate.size <= population: + # 如果有剩余人口,填充最佳棋子 + if candidate.size < population: + remaining_slots = population - candidate.size + filled_candidates = self._fill_team_with_best_chess(candidate, remaining_slots) + candidate_teams.extend(filled_candidates) + else: + candidate_teams.append(candidate) return candidate_teams @@ -338,7 +357,7 @@ class RecommendationEngine: remaining_slots: int ) -> List[TeamComposition]: """ - 用最佳棋子填充阵容剩余槽位 + 用最佳棋子填充剩余槽位 Args: base_team: 基础阵容 @@ -349,51 +368,94 @@ class RecommendationEngine: """ if remaining_slots <= 0: 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) - chess_benefits.append((chess, benefit)) + # 计算每个棋子的羁绊价值 + all_chess = self.api.get_all_chess() + 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]] - - # 创建填充后的阵容 - filled_team = TeamComposition() + # 计算基础阵容中已有的羁绊 + base_synergies = {} for chess in base_team.chess_list: - filled_team.add_chess(chess) - for chess in best_chess: - filled_team.add_chess(chess) + # 职业 + for job_id in chess.get('jobIds', '').split(','): + 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] \ No newline at end of file + # 评估每个可用棋子的价值 + 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] \ No newline at end of file diff --git a/src/recommendation_demo.py b/src/recommendation_demo.py index 44ce738..2edeeb4 100644 --- a/src/recommendation_demo.py +++ b/src/recommendation_demo.py @@ -4,20 +4,29 @@ from src.data_provider import DataQueryAPI from src.recommendation import RecommendationEngine 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("=== 云顶之弈阵容推荐模块演示 ===") # 初始化数据查询API print("\n正在加载数据...") api = DataQueryAPI() + # 获取全局权重配置 + weights_config = get_global_weights_config(config_path) + # 初始化评分系统和推荐引擎 - scoring_config = ScoringConfig() - scorer = TeamScorer(config=scoring_config) - engine = RecommendationEngine(api=api, scorer=scorer) + scorer = TeamScorer(config_path=config_path) + engine = RecommendationEngine(api=api, scorer=scorer, config_path=config_path) # 显示数据基本信息 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_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: print("\n请选择推荐方式:") @@ -32,9 +48,10 @@ def main(): print("2. 指定必选羁绊") print("3. 指定必选棋子") print("4. 综合条件推荐") - print("5. 退出") + print("5. 调整权重配置") + print("6. 退出") - choice = input("请输入选项 (1-5): ") + choice = input("请输入选项 (1-6): ") if choice == '1': try: @@ -56,6 +73,20 @@ def main(): print(f"未找到名为 '{synergy_name}' 的羁绊") 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: level = int(input(f"请输入 {synergy_name} 的目标等级 (默认为1): ") or "1") required_synergies = [{'name': synergy_name, 'level': level}] @@ -81,6 +112,20 @@ def main(): print(f"未找到名为 '{chess_name}' 的棋子") 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: population = int(input("请输入阵容人口数 (1-10,默认为9): ") or "9") if population < 1 or population > 10: @@ -112,6 +157,20 @@ def main(): print(f"未找到名为 '{synergy_name}' 的羁绊") 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") required_synergies.append({'name': synergy_name, 'level': level}) @@ -127,6 +186,20 @@ def main(): print(f"未找到名为 '{chess_name}' 的棋子") 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}) # 设置人口 @@ -147,6 +220,72 @@ def main(): print("请输入有效的整数") 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 else: @@ -200,11 +339,10 @@ def _display_recommended_teams(teams): if race: race_names.append(race['name']) - synergy_text = ' / '.join(job_names + race_names) - print(f" - {chess['displayName']} ({price}费): {synergy_text}") - - # 显示阵容总费用 - print(f"\n阵容总费用: {team.total_cost}") + job_str = ', '.join(job_names) if job_names else '无' + race_str = ', '.join(race_names) if race_names else '无' + + print(f" - {chess['displayName']} ({price}金币) | 职业: {job_str} | 特质: {race_str}") if __name__ == "__main__": diff --git a/src/scoring/__init__.py b/src/scoring/__init__.py index ace8070..306725a 100644 --- a/src/scoring/__init__.py +++ b/src/scoring/__init__.py @@ -1,7 +1,8 @@ """ -阵容评分模块 +阵容评分模块 - 提供对云顶之弈阵容的评分功能 + +此模块包含评分系统相关的组件,用于对阵容进行综合评分。 """ from .scoring_system import TeamScorer, ScoringConfig -from .config_loader import ConfigLoader, create_default_config -__all__ = ['TeamScorer', 'ScoringConfig', 'ConfigLoader', 'create_default_config'] \ No newline at end of file +__all__ = ['TeamScorer', 'ScoringConfig'] \ No newline at end of file diff --git a/src/scoring/config_loader.py b/src/scoring/config_loader.py deleted file mode 100644 index 0dc311d..0000000 --- a/src/scoring/config_loader.py +++ /dev/null @@ -1,170 +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 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 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 - }, - '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 - } - } - - 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)}") \ No newline at end of file diff --git a/src/scoring/scoring_system.py b/src/scoring/scoring_system.py index a1029a0..d2e4f61 100644 --- a/src/scoring/scoring_system.py +++ b/src/scoring/scoring_system.py @@ -12,7 +12,8 @@ from typing import Dict, List, Any, Optional import logging from dataclasses import dataclass 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( @@ -35,10 +36,10 @@ class ScoringConfig: # 棋子费用权重倍数 cost_multipliers: Dict[str, float] = None - # 自定义羁绊权重 (新增) + # 自定义羁绊权重 synergy_weights: Dict[str, float] = None - # 自定义棋子权重 (新增) + # 自定义棋子权重 chess_weights: Dict[str, float] = None def __post_init__(self): @@ -80,70 +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: - config: 评分配置,如果为None则使用默认配置 + api: 数据查询API 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")): - self._load_custom_weights(config_path) + # 加载权重配置 + if config_obj: + 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: - """ - 从配置文件加载自定义权重 - - Args: - config_path: 配置文件路径,如果为None则使用默认路径 - """ - loader = ConfigLoader(config_path) - config_data = loader.load_config() - - if not config_data: - # 如果配置文件不存在或加载失败,创建默认配置文件 - default_path = config_path or os.path.join("data", "weights_config.yaml") - create_default_config(default_path) - logger.info(f"已创建默认配置文件: {default_path}") - loader = ConfigLoader(default_path) - config_data = loader.load_config() - - # 更新基础权重 - base_weights = loader.get_base_weights() - for key, value in base_weights.items(): - if hasattr(self.config, key): - setattr(self.config, key, value) - logger.info(f"从配置文件加载基础权重: {key} = {value}") - - # 更新羁绊自定义权重 - synergy_weights = loader.get_synergy_weights() - if synergy_weights: - self.config.synergy_weights.update(synergy_weights) - logger.info(f"从配置文件加载羁绊权重: {len(synergy_weights)}个") - - # 更新棋子自定义权重 - chess_weights = loader.get_chess_weights() - if chess_weights: - self.config.chess_weights.update(chess_weights) - logger.info(f"从配置文件加载棋子权重: {len(chess_weights)}个") + def reload_config(self) -> None: + """重新加载配置""" + if isinstance(self.weights_config, dict): + # 针对直接传入的配置对象,不需要重新加载 + # 重新加载基础权重参数 + 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) - # 加载羁绊等级权重倍数 (使用新方法) - synergy_level_weights = loader.get_synergy_level_weights() - if synergy_level_weights: - # 将字符串键转换为整数键 - level_multipliers = {int(k): float(v) for k, v in synergy_level_weights.items()} - self.config.level_multipliers = level_multipliers - logger.info(f"从配置文件加载羁绊等级权重倍数: {len(level_multipliers)}个") - - # 加载棋子费用权重倍数 (使用新方法) - cost_weights = loader.get_cost_weights() - if cost_weights: - self.config.cost_multipliers = cost_weights - logger.info(f"从配置文件加载棋子费用权重倍数: {len(cost_weights)}个") + # 重新加载自定义权重 + 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()} + else: + # 获取全局配置的最新实例 + config = get_global_weights_config() + self.weights_config = config.config_data + + # 重新加载基础权重参数 + self.synergy_level_weight = config.get_base_weights().get('synergy_level_weight', 1.0) + self.synergy_count_weight = config.get_base_weights().get('synergy_count_weight', 0.5) + self.chess_cost_weight = config.get_base_weights().get('chess_cost_weight', 0.1) + + # 重新加载自定义权重 + self.synergy_weights = config.get_synergy_weights() + self.chess_weights = config.get_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: """ @@ -164,15 +176,15 @@ class TeamScorer: # 3. 棋子费用评分 chess_cost_score = self._score_chess_cost(team) - # 4. 自定义羁绊和棋子权重评分 (新增) + # 4. 自定义羁绊和棋子权重评分 custom_weight_score = self._score_custom_weights(team) # 5. 阵容整体评分 # 综合以上四项,加权求和 total_score = ( - synergy_level_score * self.config.synergy_level_weight + - synergy_count_score * self.config.synergy_count_weight + - chess_cost_score * self.config.chess_cost_weight + + synergy_level_score * self.synergy_level_weight + + synergy_count_score * self.synergy_count_weight + + chess_cost_score * self.chess_cost_weight + custom_weight_score # 自定义权重分数已经包含各自的权重 ) @@ -195,27 +207,26 @@ class TeamScorer: level = job_level['level'] 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) - level_score = level * level_multiplier + extra_units * 0.5 - score += level_score + score += level * level_multiplier + extra_units * 0.1 # 处理特质羁绊 for race_level in team.synergy_levels.get('race', []): level = race_level['level'] 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) - level_score = level * level_multiplier + extra_units * 0.5 - score += level_score + score += level * level_multiplier + extra_units * 0.1 return score def _score_synergy_count(self, team) -> float: """ - 评分羁绊种类数量 + 评分激活的羁绊种类数量 Args: team: TeamComposition实例 @@ -223,17 +234,13 @@ class TeamScorer: Returns: float: 羁绊种类数量评分 """ - # 计算激活的羁绊种类数量 - job_count = len(team.synergy_levels.get('job', [])) - race_count = len(team.synergy_levels.get('race', [])) + # 获取激活的职业和特质数量 + active_job_count = len(team.synergy_levels.get('job', [])) + active_race_count = len(team.synergy_levels.get('race', [])) - # 根据羁绊种类数量评分 - # 羁绊越多,评分越高,但有一个合理的上限 - total_count = job_count + race_count - # 使用对数函数避免羁绊数量过多时分数过高 - score = (1 + total_count) ** 0.8 - - return score + # 根据激活的羁绊种类数量计算得分 + # 激活的羁绊种类越多,得分越高,但增长是非线性的 + return (active_job_count + active_race_count) ** 0.8 def _score_chess_cost(self, team) -> float: """ @@ -247,21 +254,18 @@ class TeamScorer: """ score = 0.0 - # 按照棋子费用评分 + # 根据棋子费用计算得分 for chess in team.chess_list: - cost = chess.get('price', '1') - cost_multiplier = self.config.cost_multipliers.get(cost, 1.0) + cost = str(chess.get('price', '1')) + # 费用越高的棋子,权重越大 + cost_multiplier = self.cost_multipliers.get(cost, 1.0) score += float(cost) * cost_multiplier - # 费用分布的平衡性评分 - # 根据阵容人口,期望的费用分布可能不同 - # 这里可以进一步完善 - return score def _score_custom_weights(self, team) -> float: """ - 根据自定义权重评分 + 根据自定义权重对阵容进行评分 Args: team: TeamComposition实例 @@ -271,79 +275,140 @@ class TeamScorer: """ score = 0.0 - # 应用自定义羁绊权重 - if self.config.synergy_weights: - # 职业羁绊 - for job_level in team.synergy_levels.get('job', []): - synergy_name = job_level.get('name', '') - if synergy_name in self.config.synergy_weights: - 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}") + # 1. 评分羁绊自定义权重 + for job_level in team.synergy_levels.get('job', []): + synergy_name = job_level['name'] + if synergy_name in self.synergy_weights: + weight = self.synergy_weights[synergy_name] + score += weight * job_level['level'] - # 应用自定义棋子权重 - if self.config.chess_weights: - for chess in team.chess_list: - chess_name = chess.get('displayName', '') - if chess_name in self.config.chess_weights: - weight = self.config.chess_weights[chess_name] - score += weight - logger.debug(f"应用棋子权重: {chess_name}, 权重: {weight}") + for race_level in team.synergy_levels.get('race', []): + synergy_name = race_level['name'] + if synergy_name in self.synergy_weights: + weight = self.synergy_weights[synergy_name] + score += weight * race_level['level'] + + # 2. 评分棋子自定义权重 + 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 - + def customize_scoring(self, **kwargs) -> None: """ 自定义评分参数 Args: - **kwargs: 评分参数,如synergy_level_weight, chess_cost_weight等 + **kwargs: 参数名和值的映射 """ for key, value in kwargs.items(): - if hasattr(self.config, key): - setattr(self.config, key, value) - logger.info(f"设置评分参数 {key} = {value}") - else: - logger.warning(f"未知的评分参数: {key}") + if hasattr(self, key): + setattr(self, key, value) + logger.info(f"自定义评分参数: {key} = {value}") def set_synergy_weight(self, synergy_name: str, weight: float) -> None: """ - 设置特定羁绊的自定义权重 + 设置特定羁绊的权重 Args: synergy_name: 羁绊名称 weight: 权重值 """ - self.config.synergy_weights[synergy_name] = weight - logger.info(f"设置羁绊权重: {synergy_name} = {weight}") + self.synergy_weights[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: """ - 设置特定棋子的自定义权重 + 设置特定棋子的权重 Args: chess_name: 棋子名称 weight: 权重值 """ - self.config.chess_weights[chess_name] = weight - logger.info(f"设置棋子权重: {chess_name} = {weight}") + self.chess_weights[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: - config_path: 配置文件路径,如果为None则使用默认路径 + level: 羁绊等级 + weight: 权重值 """ - self._load_custom_weights(config_path) - logger.info("重新加载配置文件完成") \ No newline at end of file + self.level_multipliers[level] = weight + # 同时更新全局配置 + 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) \ No newline at end of file diff --git a/src/test_scoring.py b/src/test_scoring.py index e94a452..26bbd2c 100644 --- a/src/test_scoring.py +++ b/src/test_scoring.py @@ -5,12 +5,13 @@ import sys import os import logging from dataclasses import dataclass -from typing import Dict, List +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, ScoringConfig +from src.scoring.scoring_system import TeamScorer +from src.config import get_global_weights_config # 配置日志 logging.basicConfig(level=logging.INFO) @@ -22,35 +23,63 @@ class MockTeam: chess_list: List[Dict] synergy_levels: Dict -def main(config_path=None): - """主函数""" +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("基础权重:") - logger.info(f" 羁绊等级权重: {scorer.config.synergy_level_weight}") - logger.info(f" 羁绊数量权重: {scorer.config.synergy_count_weight}") - logger.info(f" 棋子费用权重: {scorer.config.chess_cost_weight}") + base_weights = weights_config.get_base_weights() + for param, value in base_weights.items(): + logger.info(f" {param}: {value}") logger.info("羁绊等级倍数:") - for level, multiplier in sorted(scorer.config.level_multipliers.items()): + 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("棋子费用倍数:") - for cost, multiplier in sorted(scorer.config.cost_multipliers.items()): + 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个):") - for name, weight in list(scorer.config.synergy_weights.items())[: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个):") - for name, weight in list(scorer.config.chess_weights.items())[: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=[ @@ -73,6 +102,15 @@ def main(config_path=None): 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__": diff --git a/src/web/app.py b/src/web/app.py index 747ad2b..0238e35 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -4,14 +4,16 @@ 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, ScoringConfig -from src.scoring.config_loader import ConfigLoader +from src.scoring.scoring_system import TeamScorer +from src.config import get_global_weights_config, WeightsConfig # 配置日志 logging.basicConfig( @@ -20,8 +22,13 @@ logging.basicConfig( ) logger = logging.getLogger("TFT-Strategist-Web") -def create_app(): - """创建Flask应用实例""" +def create_app(config_path: Optional[str] = None): + """ + 创建Flask应用实例 + + Args: + config_path: 配置文件路径,用于全局权重配置 + """ app = Flask(__name__, static_folder='static', template_folder='templates') @@ -29,10 +36,16 @@ def create_app(): # 启用CORS,允许跨域请求 CORS(app) - # 加载数据 + # 加载全局权重配置 + weights_config = get_global_weights_config(config_path) + + # 加载数据和组件 data_api = DataQueryAPI() - engine = RecommendationEngine(api=data_api) - config_loader = ConfigLoader() + 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(): @@ -43,15 +56,17 @@ def create_app(): all_synergies = all_jobs + all_races all_chess = data_api.get_all_chess() - # 获取配置文件中的权重信息 - weights_config = config_loader.load_config() + # 获取全局配置中的权重信息 + base_weights = weights_config.get_base_weights() + synergy_weights = weights_config.get_synergy_weights() + chess_weights = weights_config.get_chess_weights() return render_template('index.html', synergies=all_synergies, chess=all_chess, - base_weights=weights_config.get('base_weights', {}), - synergy_weights=weights_config.get('synergy_weights', {}), - chess_weights=weights_config.get('chess_weights', {})) + base_weights=base_weights, + synergy_weights=synergy_weights, + chess_weights=chess_weights) @app.route('/api/recommend', methods=['POST']) def recommend(): @@ -67,6 +82,20 @@ def create_app(): 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', []) @@ -75,20 +104,8 @@ def create_app(): 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] - # 创建评分配置 - scoring_config = ScoringConfig( - synergy_level_weight=base_weights.get('synergy_level_weight', 1.0), - synergy_count_weight=base_weights.get('synergy_count_weight', 0.5), - chess_cost_weight=base_weights.get('chess_cost_weight', 0.1), - synergy_weights=synergy_weights, - chess_weights=chess_weights - ) - - # 创建评分系统 - scorer = TeamScorer(config=scoring_config) - # 调用推荐引擎生成阵容 - teams = engine.recommend_team( + teams = temp_engine.recommend_team( population=population, required_synergies=required_synergies, required_chess=required_chess, @@ -153,11 +170,88 @@ def create_app(): '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 - 已弃用,仅保留API兼容性""" + try: + return jsonify({ + 'status': 'success', + 'message': '权重配置已更新(本地存储模式)' + }) + + except Exception as e: + logger.exception("更新权重配置时发生错误") + return jsonify({ + 'status': 'error', + 'message': str(e) + }), 500 + return app -def run_server(host='0.0.0.0', port=5000, dev_mode=False): - """运行Web服务器""" - app = create_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: diff --git a/src/web/static/js/main.js b/src/web/static/js/main.js index c0ecab1..aa056bb 100644 --- a/src/web/static/js/main.js +++ b/src/web/static/js/main.js @@ -2,6 +2,12 @@ // 页面加载完成后执行 $(document).ready(function() { + // 初始化抽屉 + initDrawer(); + + // 加载本地存储的权重 + loadWeightsFromLocalStorage(); + // 初始化基础权重滑块 initBasicWeightSliders(); @@ -119,16 +125,182 @@ $(document).ready(function() { $('.chess-weight-item').show(); } }); + + // 监听权重变化,保存到本地存储 + $(document).on('change', '.noUi-target', function() { + saveWeightsToLocalStorage(); + }); + + // 恢复默认配置按钮 + $('#reset-to-default').on('click', function() { + resetToDefaultWeights(); + }); }); +/** + * 初始化抽屉控制 + */ +function initDrawer() { + // 打开抽屉 + $('#open-weights-drawer').on('click', function() { + $('#weights-drawer').addClass('open'); + $('#drawer-backdrop').addClass('open'); + }); + + // 关闭抽屉 + $('#close-drawer, #drawer-backdrop').on('click', function() { + $('#weights-drawer').removeClass('open'); + $('#drawer-backdrop').removeClass('open'); + }); +} + +/** + * 从本地存储加载权重设置 + */ +function loadWeightsFromLocalStorage() { + const storedWeights = localStorage.getItem('tft-weights'); + if (!storedWeights) return; + + try { + const weights = JSON.parse(storedWeights); + + // 设置基础权重 + if (weights.base_weights) { + if (weights.base_weights.synergy_level_weight) { + $('#synergy-level-weight-value').text(weights.base_weights.synergy_level_weight.toFixed(1)); + } + if (weights.base_weights.synergy_count_weight) { + $('#synergy-count-weight-value').text(weights.base_weights.synergy_count_weight.toFixed(1)); + } + if (weights.base_weights.chess_cost_weight) { + $('#chess-cost-weight-value').text(weights.base_weights.chess_cost_weight.toFixed(1)); + } + } + + // 设置羁绊权重 + if (weights.synergy_weights) { + for (const [synergyId, weight] of Object.entries(weights.synergy_weights)) { + $(`.synergy-weight-item[data-synergy-id="${synergyId}"] .synergy-weight-value`).text(weight.toFixed(1)); + } + } + + // 设置棋子权重 + if (weights.chess_weights) { + for (const [chessId, weight] of Object.entries(weights.chess_weights)) { + $(`.chess-weight-item[data-chess-id="${chessId}"] .chess-weight-value`).text(weight.toFixed(1)); + } + } + } catch (e) { + console.error('加载权重设置失败:', e); + } +} + +/** + * 保存权重设置到本地存储 + */ +function saveWeightsToLocalStorage() { + // 获取基础权重 + const synergyLevelWeight = parseFloat($('#synergy-level-weight-value').text()); + const synergyCountWeight = parseFloat($('#synergy-count-weight-value').text()); + const chessCountWeight = parseFloat($('#chess-cost-weight-value').text()); + + // 获取羁绊权重 + const synergyWeights = {}; + $('.synergy-weight-item').each(function() { + const synergyId = $(this).data('synergy-id'); + const weight = parseFloat($(this).find('.synergy-weight-value').text()); + synergyWeights[synergyId] = weight; + }); + + // 获取棋子权重 + const chessWeights = {}; + $('.chess-weight-item').each(function() { + const chessId = $(this).data('chess-id'); + const weight = parseFloat($(this).find('.chess-weight-value').text()); + chessWeights[chessId] = weight; + }); + + // 构建权重对象 + const weightsObj = { + base_weights: { + synergy_level_weight: synergyLevelWeight, + synergy_count_weight: synergyCountWeight, + chess_cost_weight: chessCountWeight + }, + synergy_weights: synergyWeights, + chess_weights: chessWeights + }; + + // 保存到本地存储 + localStorage.setItem('tft-weights', JSON.stringify(weightsObj)); +} + +/** + * 重置为默认权重 + */ +function resetToDefaultWeights() { + // 发送请求获取默认权重 + $.ajax({ + url: '/api/weights', + type: 'GET', + success: function(response) { + // 更新基础权重 + if (response.base_weights) { + if (response.base_weights.synergy_level_weight) { + const slider = document.getElementById('synergy-level-weight-slider').noUiSlider; + slider.set(response.base_weights.synergy_level_weight); + } + if (response.base_weights.synergy_count_weight) { + const slider = document.getElementById('synergy-count-weight-slider').noUiSlider; + slider.set(response.base_weights.synergy_count_weight); + } + if (response.base_weights.chess_cost_weight) { + const slider = document.getElementById('chess-cost-weight-slider').noUiSlider; + slider.set(response.base_weights.chess_cost_weight); + } + } + + // 更新羁绊权重 + if (response.synergy_weights) { + for (const [synergyId, weight] of Object.entries(response.synergy_weights)) { + const slider = $(`.synergy-weight-item[data-synergy-id="${synergyId}"] .synergy-weight-slider`)[0]; + if (slider && slider.noUiSlider) { + slider.noUiSlider.set(weight); + } + } + } + + // 更新棋子权重 + if (response.chess_weights) { + for (const [chessId, weight] of Object.entries(response.chess_weights)) { + const slider = $(`.chess-weight-item[data-chess-id="${chessId}"] .chess-weight-slider`)[0]; + if (slider && slider.noUiSlider) { + slider.noUiSlider.set(weight); + } + } + } + + // 清除本地存储 + localStorage.removeItem('tft-weights'); + + alert('已恢复默认权重设置'); + }, + error: function(xhr, status, error) { + alert(`恢复默认权重失败: ${error}`); + } + }); +} + /** * 初始化基础权重滑块 */ function initBasicWeightSliders() { // 羁绊等级权重 const synergyLevelSlider = document.getElementById('synergy-level-weight-slider'); + const synergyLevelValue = parseFloat($('#synergy-level-weight-value').text()); + noUiSlider.create(synergyLevelSlider, { - start: [1.0], + start: [synergyLevelValue], connect: [true, false], step: 0.1, range: { @@ -144,8 +316,10 @@ function initBasicWeightSliders() { // 羁绊数量权重 const synergyCountSlider = document.getElementById('synergy-count-weight-slider'); + const synergyCountValue = parseFloat($('#synergy-count-weight-value').text()); + noUiSlider.create(synergyCountSlider, { - start: [0.5], + start: [synergyCountValue], connect: [true, false], step: 0.1, range: { @@ -161,8 +335,10 @@ function initBasicWeightSliders() { // 棋子费用权重 const chessCountSlider = document.getElementById('chess-cost-weight-slider'); + const chessCountValue = parseFloat($('#chess-cost-weight-value').text()); + noUiSlider.create(chessCountSlider, { - start: [0.1], + start: [chessCountValue], connect: [true, false], step: 0.1, range: { @@ -289,6 +465,9 @@ function generateTeam() { required_chess: requiredChess }; + // 保存当前设置到本地存储 + saveWeightsToLocalStorage(); + // 发送请求 $.ajax({ url: '/api/recommend', diff --git a/src/web/templates/index.html b/src/web/templates/index.html index c5fb33c..335a9d3 100644 --- a/src/web/templates/index.html +++ b/src/web/templates/index.html @@ -13,14 +13,139 @@ + + +
+ + +
+
+

权重设置

+ +
+ +
+ +
+ +

基础权重

+ +
+ +
+
+ 0.0 + 1.0 + 3.0 +
+
+ +
+ +
+
+ 0.0 + 0.5 + 3.0 +
+
+ +
+ +
+
+ 0.0 + 0.1 + 1.0 +
+
+ +
+

羁绊权重设置

+
+ + +
+
+ +
+ {% for synergy_id, weight in synergy_weights.items() %} +
+
+ + {{ weight }} +
+
+
+ {% endfor %} +
+ +
+

棋子权重设置

+
+ + +
+
+ +
+ {% for chess_id, weight in chess_weights.items() %} +
+
+ + {{ weight }} +
+
+
+ {% endfor %} +
+
+

云顶之弈阵容推荐器

-
- +
+

基础设置

@@ -42,36 +167,10 @@
-

基础权重

- -
- -
-
- 0.0 - 1.0 - 3.0 -
-
- -
- -
-
- 0.0 - 0.5 - 3.0 -
-
- -
- -
-
- 0.0 - 0.1 - 1.0 -
+
+
@@ -110,49 +209,6 @@
- -
-
-

羁绊权重设置

-
- - -
-
- -
- {% for synergy_id, weight in synergy_weights.items() %} -
-
- - {{ weight }} -
-
-
- {% endfor %} -
- -
-

棋子权重设置

-
- - -
-
- -
- {% for chess_id, weight in chess_weights.items() %} -
-
- - {{ weight }} -
-
-
- {% endfor %} -
-
-

阵容推荐结果

diff --git a/test_direct.py b/test_direct.py new file mode 100644 index 0000000..f30ed06 --- /dev/null +++ b/test_direct.py @@ -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() \ No newline at end of file diff --git a/test_recommendation.py b/test_recommendation.py new file mode 100644 index 0000000..7f8af06 --- /dev/null +++ b/test_recommendation.py @@ -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() \ No newline at end of file