数据提供模块编写
This commit is contained in:
parent
125dfc3507
commit
b014895061
149
README.md
Normal file
149
README.md
Normal file
@ -0,0 +1,149 @@
|
||||
# 云顶之弈阵容推荐器
|
||||
|
||||
## 项目概述
|
||||
|
||||
本项目是一个基于Python的云顶之弈阵容推荐工具,通过Riot官方提供的游戏数据(职业、特质、棋子),实现自动化的阵容分析与推荐。项目具有灵活的评分机制,能够根据用户需求(如指定人口、羁绊、棋子等)智能推荐最优阵容。
|
||||
|
||||
## 主要功能
|
||||
|
||||
1. **数据提供模块**
|
||||
- 从官方接口获取最新游戏数据并支持本地缓存
|
||||
- 提供丰富的数据查询API,如获取羁绊下的棋子、棋子的羁绊详情等
|
||||
- 高效的数据处理与缓存机制
|
||||
|
||||
2. **阵容推荐模块** (开发中)
|
||||
- 根据用户指定的人口、必须羁绊、必选棋子自动生成阵容
|
||||
- 支持多种约束条件组合,如指定多个羁绊、多个棋子等
|
||||
|
||||
3. **阵容评分模块** (开发中)
|
||||
- 综合考虑羁绊数量、等级、棋子费用等因素
|
||||
- 可配置的评分权重,支持自定义评分策略
|
||||
|
||||
4. **接口模块** (开发中)
|
||||
- 提供编程接口,方便集成到其他应用中
|
||||
- 命令行界面,便于直接使用
|
||||
|
||||
## 环境与依赖
|
||||
|
||||
- **Python版本:** Python 3.8+
|
||||
- **主要依赖:**
|
||||
- requests: 用于获取在线数据
|
||||
- pandas: 用于数据处理
|
||||
- pyyaml: 用于配置文件解析
|
||||
- pytest: 用于单元测试
|
||||
|
||||
## 项目安装
|
||||
|
||||
1. 克隆项目代码
|
||||
```bash
|
||||
git clone https://github.com/username/TFT-Strategist.git
|
||||
cd TFT-Strategist
|
||||
```
|
||||
|
||||
2. 安装依赖
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 数据提供模块演示
|
||||
|
||||
运行以下命令启动数据提供模块的演示程序:
|
||||
|
||||
```bash
|
||||
python main.py data
|
||||
```
|
||||
|
||||
演示程序将展示以下功能:
|
||||
- 加载最新的游戏数据
|
||||
- 显示当前版本职业、特质和棋子的基本信息
|
||||
- 提供交互式查询界面,可以查询羁绊的棋子、棋子的羁绊等信息
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
TFT-Strategist/
|
||||
├── data/ # 数据存储目录
|
||||
│ ├── chess.json # 棋子数据
|
||||
│ ├── job.json # 职业数据
|
||||
│ └── race.json # 特质数据
|
||||
├── src/ # 源代码
|
||||
│ ├── data_provider/ # 数据提供模块
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── data_loader.py # 数据加载器
|
||||
│ │ └── data_query_api.py # 数据查询API
|
||||
│ ├── recommendation/ # 阵容推荐模块 (开发中)
|
||||
│ ├── scoring/ # 阵容评分模块 (开发中)
|
||||
│ ├── interface/ # 接口模块 (开发中)
|
||||
│ ├── __init__.py
|
||||
│ └── data_provider_demo.py # 数据提供模块演示
|
||||
├── tests/ # 测试代码
|
||||
│ └── test_data_provider.py # 数据提供模块测试
|
||||
├── config/ # 配置文件目录
|
||||
├── main.py # 主程序入口
|
||||
├── requirements.txt # 项目依赖
|
||||
└── README.md # 项目文档
|
||||
```
|
||||
|
||||
## 数据提供模块详解
|
||||
|
||||
数据提供模块是整个阵容推荐系统的基础,负责从官方接口获取最新的游戏数据,并提供丰富的查询功能。
|
||||
|
||||
### 主要组件
|
||||
|
||||
1. **DataLoader**: 负责数据获取与缓存
|
||||
- 支持从本地或网络加载数据
|
||||
- 实现数据缓存机制,避免频繁请求
|
||||
- 自动处理数据更新
|
||||
|
||||
2. **DataQueryAPI**: 提供丰富的数据查询接口
|
||||
- 获取所有职业/特质/棋子数据
|
||||
- 根据ID或名称查询具体数据
|
||||
- 查询羁绊下的棋子、棋子的羁绊
|
||||
- 获取羁绊等级信息
|
||||
- 查询棋子费用分布
|
||||
|
||||
### 使用示例
|
||||
|
||||
```python
|
||||
from src.data_provider import DataQueryAPI
|
||||
|
||||
# 初始化API
|
||||
api = DataQueryAPI()
|
||||
|
||||
# 获取所有职业
|
||||
jobs = api.get_all_jobs()
|
||||
|
||||
# 查询特定羁绊的棋子
|
||||
heavy_warrior = api.get_job_by_name("重装战士")
|
||||
if heavy_warrior:
|
||||
chess_list = api.get_chess_by_job(heavy_warrior['jobId'])
|
||||
print(f"重装战士棋子: {[chess['displayName'] for chess in chess_list]}")
|
||||
|
||||
# 查询棋子的羁绊
|
||||
brand = api.get_chess_by_name("布兰德")
|
||||
if brand:
|
||||
synergies = api.get_synergies_of_chess(brand['chessId'])
|
||||
print(f"布兰德的羁绊: {[synergy['name'] for synergy in synergies]}")
|
||||
```
|
||||
|
||||
## 开发计划
|
||||
|
||||
- [x] 数据提供模块
|
||||
- [ ] 阵容推荐模块
|
||||
- [ ] 阵容评分模块
|
||||
- [ ] 接口模块
|
||||
- [ ] 图形用户界面
|
||||
|
||||
## 贡献指南
|
||||
|
||||
欢迎贡献代码、报告问题或提出新功能建议!请先fork本仓库,然后提交拉取请求。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有任何问题或建议,请提交issue或联系项目维护者。
|
2376
data/chess.json
Normal file
2376
data/chess.json
Normal file
File diff suppressed because it is too large
Load Diff
213
data/job.json
Normal file
213
data/job.json
Normal file
@ -0,0 +1,213 @@
|
||||
{
|
||||
"version": "15.7",
|
||||
"season": "2025.S14",
|
||||
"modeId": "1",
|
||||
"time": "2025-04-01 14:31:07",
|
||||
"data": [
|
||||
{
|
||||
"jobId": "10155",
|
||||
"name": "超频战士",
|
||||
"traitId": "10155",
|
||||
"introduce": "【超频战士】们可以通过该羁绊专有的【超频】属性来给他们的技能提供独特增强。",
|
||||
"alias": "10155.png",
|
||||
"level": {
|
||||
"2": "+1 【超频】属性, 100 生命值",
|
||||
"3": "+2 【超频】属性, 200 生命值",
|
||||
"4": "+3 【超频】属性, 350 生命值",
|
||||
"5": "+4 【超频】属性, 500 生命值"
|
||||
},
|
||||
"TFTID": "10155",
|
||||
"characterid": "TFT14_Supercharge",
|
||||
"id": "2623",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/classes/10155.png",
|
||||
"job_color_list": "2:1,3:2,4:3,5:3"
|
||||
},
|
||||
{
|
||||
"jobId": "10157",
|
||||
"name": "重装战士",
|
||||
"traitId": "10157",
|
||||
"introduce": "【重装战士】们在有护盾时获得10%伤害减免。\r\n战斗开始时和50%生命值时:获得一部分最大生命值的护盾值,持续10秒。",
|
||||
"alias": "10157.png",
|
||||
"level": {
|
||||
"2": "16%最大生命值",
|
||||
"4": "32%最大生命值",
|
||||
"6": "40%最大生命值;在有护盾时获得16% 伤害减免"
|
||||
},
|
||||
"TFTID": "10157",
|
||||
"characterid": "TFT14_Vanguard",
|
||||
"id": "2624",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/classes/10157.png",
|
||||
"job_color_list": "2:1,4:2,6:3"
|
||||
},
|
||||
{
|
||||
"jobId": "10158",
|
||||
"name": "杀手",
|
||||
"traitId": "10158",
|
||||
"introduce": "【杀手】们获得攻击力和全能吸血。过量治疗将转而为百分比生命值最低的【杀手】回复50%的溢出额。",
|
||||
"alias": "10158.png",
|
||||
"level": {
|
||||
"2": "15% 攻击力, 15% 全能吸血",
|
||||
"4": "40% 攻击力, 15% 全能吸血",
|
||||
"6": "70% 攻击力, 20% 全能吸血"
|
||||
},
|
||||
"TFTID": "10158",
|
||||
"characterid": "TFT14_Strong",
|
||||
"id": "2625",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/classes/10158.png",
|
||||
"job_color_list": "2:1,4:2,6:3"
|
||||
},
|
||||
{
|
||||
"jobId": "10161",
|
||||
"name": "裁决使",
|
||||
"traitId": "10161",
|
||||
"introduce": "【裁决使】的技能可以暴击。他们会获得额外的暴击几率和暴击伤害。\r\n如果目标的生命值低于20%,这个加成的暴击伤害会翻倍。",
|
||||
"alias": "10161.png",
|
||||
"level": {
|
||||
"2": "25% 暴击几率,5% 暴击伤害",
|
||||
"3": "35% 暴击几率,10% 暴击伤害",
|
||||
"4": "45% 暴击几率,15% 暴击伤害",
|
||||
"5": "50% 暴击几率,15% 暴击伤害;还会获得12%伤害减免"
|
||||
},
|
||||
"TFTID": "10161",
|
||||
"characterid": "TFT14_Cutter",
|
||||
"id": "2626",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/classes/10161.png",
|
||||
"job_color_list": "2:1,3:2,4:3,5:3"
|
||||
},
|
||||
{
|
||||
"jobId": "10162",
|
||||
"name": "堡垒卫士",
|
||||
"traitId": "10162",
|
||||
"introduce": "你的小队获得10护甲和魔法抗性。【堡垒卫士】们获得更多。\r\n战斗开始后的最初10秒,【堡垒卫士】们将其加成提升100%。",
|
||||
"alias": "10162.png",
|
||||
"level": {
|
||||
"2": "18 护甲和魔抗",
|
||||
"4": "40 护甲和魔抗",
|
||||
"6": "70 护甲和魔抗 非【堡垒卫士】弈子们获得额外的30 护甲和魔抗。"
|
||||
},
|
||||
"TFTID": "10162",
|
||||
"characterid": "TFT14_Armorclad",
|
||||
"id": "2627",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/classes/10162.png",
|
||||
"job_color_list": "2:1,4:2,6:3"
|
||||
},
|
||||
{
|
||||
"jobId": "10166",
|
||||
"name": "迅捷射手",
|
||||
"traitId": "10166",
|
||||
"introduce": "你的小队获得10%攻击速度。【迅捷射手】们在每次攻击时获得更多,至多可叠加10次。",
|
||||
"alias": "10166.png",
|
||||
"level": {
|
||||
"2": "每层4% 攻击速度",
|
||||
"4": "每层10% 攻击速度",
|
||||
"6": "每层24% 攻击速度"
|
||||
},
|
||||
"TFTID": "10166",
|
||||
"characterid": "TFT14_Swift",
|
||||
"id": "2628",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/classes/10166.png",
|
||||
"job_color_list": "2:1,4:2,6:3"
|
||||
},
|
||||
{
|
||||
"jobId": "10167",
|
||||
"name": "战略分析师",
|
||||
"traitId": "10167",
|
||||
"introduce": "战斗开始时:后2排的友军获得伤害增幅。前2排的友军获得伤害减免。【战略分析师】们获得三倍。",
|
||||
"alias": "10167.png",
|
||||
"level": {
|
||||
"2": "6% 伤害增幅,4% 伤害减免",
|
||||
"3": "9% 伤害增幅,6% 伤害减免",
|
||||
"4": "12% 伤害增幅,8% 伤害减免",
|
||||
"5": "15% 伤害增幅,10% 伤害减免"
|
||||
},
|
||||
"TFTID": "10167",
|
||||
"characterid": "TFT14_Controller",
|
||||
"id": "2629",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/classes/10167.png",
|
||||
"job_color_list": "2:1,3:2,4:3,5:3"
|
||||
},
|
||||
{
|
||||
"jobId": "10168",
|
||||
"name": "斗士",
|
||||
"traitId": "10168",
|
||||
"introduce": "你的小队获得100生命值。【斗士】们获得更多。",
|
||||
"alias": "10168.png",
|
||||
"level": {
|
||||
"2": "20% 生命值",
|
||||
"4": "45% 生命值",
|
||||
"6": "70% 生命值"
|
||||
},
|
||||
"TFTID": "10168",
|
||||
"characterid": "TFT14_Bruiser",
|
||||
"id": "2630",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/classes/10168.png",
|
||||
"job_color_list": "2:1,4:2,6:3"
|
||||
},
|
||||
{
|
||||
"jobId": "10169",
|
||||
"name": "强袭射手",
|
||||
"traitId": "10169",
|
||||
"introduce": "【强袭射手】们获得攻击力。在战斗8秒后,他们将其加成提升100%。",
|
||||
"alias": "10169.png",
|
||||
"level": {
|
||||
"2": "18% 攻击力",
|
||||
"4": "35% 攻击力。在8秒后,每6秒获得20% 攻击力。"
|
||||
},
|
||||
"TFTID": "10169",
|
||||
"characterid": "TFT14_Marksman",
|
||||
"id": "2631",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/classes/10169.png",
|
||||
"job_color_list": "2:1,4:3"
|
||||
},
|
||||
{
|
||||
"jobId": "10172",
|
||||
"name": "高级工程师",
|
||||
"traitId": "10172",
|
||||
"introduce": "【高级工程师】们获得法术强度。\r\n他们的技能在命中敌人们后,会对这些敌人造成持续3秒的10%伤害削减。",
|
||||
"alias": "10172.png",
|
||||
"level": {
|
||||
"2": "20 法术强度",
|
||||
"4": "50 法术强度",
|
||||
"6": "85 法术强度",
|
||||
"8": "125 法术强度 对命中的敌人们造成18%伤害削减。"
|
||||
},
|
||||
"TFTID": "10172",
|
||||
"characterid": "TFT14_Techie",
|
||||
"id": "2632",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/classes/10172.png",
|
||||
"job_color_list": "2:1,4:2,6:3,8:4"
|
||||
},
|
||||
{
|
||||
"jobId": "10173",
|
||||
"name": "人造人",
|
||||
"traitId": "10173",
|
||||
"introduce": "每3秒,你的小队获得法力值。【人造人】们多获得100%。",
|
||||
"alias": "10173.png",
|
||||
"level": {
|
||||
"2": "5 法力值",
|
||||
"3": "7 法力值",
|
||||
"4": "10 法力值"
|
||||
},
|
||||
"TFTID": "10173",
|
||||
"characterid": "TFT14_Thirsty",
|
||||
"id": "2633",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/classes/10173.png",
|
||||
"job_color_list": "2:1,3:2,4:3"
|
||||
},
|
||||
{
|
||||
"jobId": "10178",
|
||||
"name": "召唤物",
|
||||
"traitId": "10178",
|
||||
"introduce": "召唤物",
|
||||
"alias": "10178.png",
|
||||
"level": {
|
||||
"1": "召唤物"
|
||||
},
|
||||
"TFTID": "10178",
|
||||
"characterid": "",
|
||||
"id": "2634",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/classes/10178.png",
|
||||
"job_color_list": "1:1"
|
||||
}
|
||||
]
|
||||
}
|
244
data/race.json
Normal file
244
data/race.json
Normal file
@ -0,0 +1,244 @@
|
||||
{
|
||||
"version": "15.7",
|
||||
"season": "2025.S14",
|
||||
"modeId": "1",
|
||||
"time": "2025-04-01 14:31:07",
|
||||
"data": [
|
||||
{
|
||||
"raceId": "10153",
|
||||
"name": "圣灵使者",
|
||||
"traitId": "10153",
|
||||
"introduce": "【圣灵使者】弈子会为你的小队提供独特的属性,该加成会随着已登场的【圣灵使者】弈子数量而提升。\r\n【圣灵使者】弈子们获得双倍。",
|
||||
"alias": "10153.png",
|
||||
"level": {
|
||||
"1": "100% 加成。",
|
||||
"2": "110% 加成。",
|
||||
"3": "125% 加成。",
|
||||
"4": "140% 加成。",
|
||||
"5": "160% 加成。",
|
||||
"6": "180% 加成。",
|
||||
"7": "200% 加成。"
|
||||
},
|
||||
"TFTID": "10153",
|
||||
"characterid": "TFT14_Divinicorp",
|
||||
"id": "2727",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10153.png",
|
||||
"race_color_list": "1:1,2:2,3:2,4:3,5:3,6:3,7:4"
|
||||
},
|
||||
{
|
||||
"raceId": "10154",
|
||||
"name": "街头恶魔",
|
||||
"traitId": "10154",
|
||||
"introduce": "在【彩绘格】中的友军将获得生命值、法术强度和攻击力。一些格子是【签名格】并多提供50%此加成。\r\n【街头恶魔】使所有此加成翻倍。",
|
||||
"alias": "10154.png",
|
||||
"level": {
|
||||
"3": "+6% 生命值,6 法术强度,6% 攻击力",
|
||||
"5": "+10% 生命值,10 法术强度,10% 攻击力",
|
||||
"7": "+6% 生命值,15 法术强度,15% 攻击力",
|
||||
"10": "+50% 生命值,50 法术强度,50% 攻击力,尽情绘画!"
|
||||
},
|
||||
"TFTID": "10154",
|
||||
"characterid": "TFT14_StreetDemon",
|
||||
"id": "2728",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10154.png",
|
||||
"race_color_list": "3:1,5:2,7:3,10:4"
|
||||
},
|
||||
{
|
||||
"raceId": "10156",
|
||||
"name": "幻灵战队",
|
||||
"traitId": "10156",
|
||||
"introduce": "每个层级,挑出一件随机【幻灵战队】武器,用于在战斗期间周期性开火。【幻灵战队】弈子们获得护甲、魔抗和伤害增幅。",
|
||||
"alias": "10156.png",
|
||||
"level": {
|
||||
"3": "10 护甲和魔抗, 5% 伤害增幅,选择一件武器",
|
||||
"5": "30 护甲和魔抗, 12% 伤害增幅,选择一件武器",
|
||||
"7": "45 护甲和魔抗, 20% 伤害增幅,选择一件武器",
|
||||
"10": "75 护甲和魔抗, 25% 伤害增幅,选择一件终极武器。"
|
||||
},
|
||||
"TFTID": "10156",
|
||||
"characterid": "TFT14_AnimaSquad",
|
||||
"id": "2729",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10156.png",
|
||||
"race_color_list": "3:1,5:2,7:3,10:4"
|
||||
},
|
||||
{
|
||||
"raceId": "10159",
|
||||
"name": "弑魂者",
|
||||
"traitId": "10159",
|
||||
"introduce": "获得一个全息投影,模仿【佛耶戈】上回合参与击杀的最高费敌人。投影拥有900/1350/9001生命值,造成30%/40%/200%伤害,并拥有1件推荐装备。",
|
||||
"alias": "10159.png",
|
||||
"level": {
|
||||
"1": "获得一个全息投影,模仿【佛耶戈】上回合参与击杀的最高费敌人。投影拥有900/1350/9001生命值,造成30%/40%/200%伤害,并拥有1件推荐装备。"
|
||||
},
|
||||
"TFTID": "10159",
|
||||
"characterid": "TFT14_ViegoUniqueTrait",
|
||||
"id": "2730",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10159.png",
|
||||
"race_color_list": "1:5"
|
||||
},
|
||||
{
|
||||
"raceId": "10160",
|
||||
"name": "赛博老大",
|
||||
"traitId": "10160",
|
||||
"introduce": "你最强大的那个【赛博老大】会升级为它的最终形态并获得生命值、法术强度、并且它的技能会命中更多敌人。",
|
||||
"alias": "10160.png",
|
||||
"level": {
|
||||
"2": "25% 生命值, 20 法术强度",
|
||||
"3": "33% 生命值, 30 法术强度",
|
||||
"4": "所有【赛博老大】升级。你最强大的那个【赛博老大】获得40% 生命值和40 法术强度"
|
||||
},
|
||||
"TFTID": "10160",
|
||||
"characterid": "TFT14_Cyberboss",
|
||||
"id": "2731",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10160.png",
|
||||
"race_color_list": "2:1,3:2,4:3"
|
||||
},
|
||||
{
|
||||
"raceId": "10163",
|
||||
"name": "源计划",
|
||||
"traitId": "10163",
|
||||
"introduce": "获得仅供【源计划】弈子们携带的独特装备。他们每携带一件装备就会获得生命值和攻击速度。",
|
||||
"alias": "10163.png",
|
||||
"level": {
|
||||
"3": "50 生命值, 2% 攻击速度,第一件源计划装备",
|
||||
"5": "150 生命值, 5% 攻击速度,第二件源计划装备",
|
||||
"7": "225 生命值, 9% 攻击速度,第三件源计划装备",
|
||||
"10": "500 生命值, 40% 攻击速度,全部源计划装备"
|
||||
},
|
||||
"TFTID": "10163",
|
||||
"characterid": "TFT14_EdgeRunner",
|
||||
"id": "2732",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10163.png",
|
||||
"race_color_list": "3:1,5:2,7:3,10:4"
|
||||
},
|
||||
{
|
||||
"raceId": "10164",
|
||||
"name": "鳄霸",
|
||||
"traitId": "10164",
|
||||
"introduce": "【鳄霸】咬一口他身后格子内的弈子,造成该弈子40%最大生命值的真实伤害。他获得该弈子的40%生命值和该弈子的33%攻击力。",
|
||||
"alias": "10164.png",
|
||||
"level": {
|
||||
"1": "【鳄霸】咬一口他身后格子内的弈子,造成该弈子40%最大生命值的真实伤害。他获得该弈子的40%生命值和该弈子的33%攻击力。"
|
||||
},
|
||||
"TFTID": "10164",
|
||||
"characterid": "TFT14_Overlord",
|
||||
"id": "2733",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10164.png",
|
||||
"race_color_list": "1:5"
|
||||
},
|
||||
{
|
||||
"raceId": "10165",
|
||||
"name": "网络之神",
|
||||
"traitId": "10165",
|
||||
"introduce": "2个玩家对战回合后,打开一个武器库,内含的羁绊改件可永久对一个弈子进行重新编程,使其受益于一个羁绊(但不贡献羁绊计数)。\r\n你每次获得一个【羁绊改件】时,下一个所需的回合数就需要增加1。\r\n弈子仅能拥有一个【羁绊改件】。",
|
||||
"alias": "10165.png",
|
||||
"level": {
|
||||
"1": "2个玩家对战回合后,打开一个武器库,内含的羁绊改件可永久对一个弈子进行重新编程,使其受益于一个羁绊(但不贡献羁绊计数)。你每次获得一个【羁绊改件】时,下一个所需的回合数就需要增加1。弈子仅能拥有一个【羁绊改件】。"
|
||||
},
|
||||
"TFTID": "10165",
|
||||
"characterid": "TFT14_Netgod",
|
||||
"id": "2734",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10165.png",
|
||||
"race_color_list": "1:5"
|
||||
},
|
||||
{
|
||||
"raceId": "10170",
|
||||
"name": "福牛守护者",
|
||||
"traitId": "10170",
|
||||
"introduce": "【福牛守护者】们获得伤害增幅,并且在击杀时有一定几率掉落金币。\r\n如果你在单个回合内花费8金币用于刷新或经验值,则会永久提升其伤害增幅以及下次加成所需的金币数量。在计算金币花费时,刷新所用的金币算作双倍。\r\n(【福牛守护者】必须在上个回合进行过战斗)",
|
||||
"alias": "10170.png",
|
||||
"level": {
|
||||
"2": "13% 伤害增幅,20%金币",
|
||||
"4": "20% 伤害增幅,50%金币",
|
||||
"6": "25% 伤害增幅,75%战利品,战利品有7%几率是一件基础装备"
|
||||
},
|
||||
"TFTID": "10170",
|
||||
"characterid": "TFT14_Immortal",
|
||||
"id": "2735",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10170.png",
|
||||
"race_color_list": "2:1,4:2,6:3"
|
||||
},
|
||||
{
|
||||
"raceId": "10171",
|
||||
"name": "战地机甲",
|
||||
"traitId": "10171",
|
||||
"introduce": "【战地机甲】们每造成400伤害,就会向一个附近的敌人发射一颗造成魔法伤害的导弹。8%的承受伤害会被计入造成伤害。",
|
||||
"alias": "10171.png",
|
||||
"level": {
|
||||
"2": "150魔法伤害",
|
||||
"4": "325魔法伤害",
|
||||
"6": "发射两颗导弹,每颗造成225魔法伤害"
|
||||
},
|
||||
"TFTID": "10171",
|
||||
"characterid": "TFT14_BallisTek",
|
||||
"id": "2736",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10171.png",
|
||||
"race_color_list": "2:1,4:2,6:3"
|
||||
},
|
||||
{
|
||||
"raceId": "10174",
|
||||
"name": "病毒魔人",
|
||||
"traitId": "10174",
|
||||
"introduce": "【病毒魔人】有10%几率感染你的商店,感染时会生成一个细胞组织。购买后,细胞组织会合并并使最强大的那个【扎克】提升3%最大生命值和4法术强度。",
|
||||
"alias": "10174.png",
|
||||
"level": {
|
||||
"1": "【病毒魔人】有10%几率感染你的商店,感染时会生成一个细胞组织。购买后,细胞组织会合并并使最强大的那个【扎克】提升3%最大生命值和4法术强度。"
|
||||
},
|
||||
"TFTID": "10174",
|
||||
"characterid": "TFT14_Virus",
|
||||
"id": "2737",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10174.png",
|
||||
"race_color_list": "1:5"
|
||||
},
|
||||
{
|
||||
"raceId": "10175",
|
||||
"name": "辛迪加",
|
||||
"traitId": "10175",
|
||||
"introduce": "获得一个【主理人之帽】,用于将一个【辛迪加】弈子的技能进行独特升级。\r\n【辛迪加】弈子们获得生命值和伤害增幅。",
|
||||
"alias": "10175.png",
|
||||
"level": {
|
||||
"3": "100 生命值,5% 伤害增幅,1个【主理人】",
|
||||
"5": "400 生命值,15% 伤害增幅,2个【主理人】",
|
||||
"7": "500 生命值,20% 伤害增幅,升级【主理人】效果。"
|
||||
},
|
||||
"TFTID": "10175",
|
||||
"characterid": "TFT14_Mob",
|
||||
"id": "2738",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10175.png",
|
||||
"race_color_list": "3:1,5:2,7:3"
|
||||
},
|
||||
{
|
||||
"raceId": "10176",
|
||||
"name": "魔装机神",
|
||||
"traitId": "10176",
|
||||
"introduce": "每回合,独特的【魔装机神】们都会给R-080T提供基于各自星级的【铬】。每份【铬】提供14生命值和1法术强度。\r\n在200份【铬】时,它会升级为T-43X!",
|
||||
"alias": "10176.png",
|
||||
"level": {
|
||||
"3": "召唤R-080T!",
|
||||
"4": "发射一束巨大激光!"
|
||||
},
|
||||
"TFTID": "10176",
|
||||
"characterid": "TFT14_HotRod",
|
||||
"id": "2739",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10176.png",
|
||||
"race_color_list": "3:1,4:3"
|
||||
},
|
||||
{
|
||||
"raceId": "10177",
|
||||
"name": "执事",
|
||||
"traitId": "10177",
|
||||
"introduce": "通过输掉战斗获得【情报】,获取量会在连败时提升。击杀敌人时获得少量【情报】。\r\n你可以用你的【情报】进行一次战利品交易,在以下回合进行交易:\r\n3-3、3-7、4-3, 4-7、或5-5。\r\n在交易【情报】后,【执事】们获得攻击力和法术强度。",
|
||||
"alias": "10177.png",
|
||||
"level": {
|
||||
"3": "1x【情报】,30% 攻击力和法术强度",
|
||||
"4": "1.5x【情报】,45% 攻击力和法术强度",
|
||||
"5": "2x【情报】,55% 攻击力和法术强度"
|
||||
},
|
||||
"TFTID": "10177",
|
||||
"characterid": "TFT14_Suits",
|
||||
"id": "2740",
|
||||
"imagePath": "https://game.gtimg.cn/images/lol/act/img/tft/origins/10177.png",
|
||||
"race_color_list": "3:1,4:2,5:3"
|
||||
}
|
||||
]
|
||||
}
|
33
main.py
Normal file
33
main.py
Normal file
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
云顶之弈阵容推荐器 - 主程序
|
||||
"""
|
||||
import sys
|
||||
from src.data_provider_demo import main as data_provider_demo
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 检查命令行参数
|
||||
if len(sys.argv) > 1:
|
||||
command = sys.argv[1]
|
||||
if command == "data":
|
||||
# 运行数据提供模块演示
|
||||
data_provider_demo()
|
||||
else:
|
||||
print(f"未知命令: {command}")
|
||||
print_usage()
|
||||
else:
|
||||
# 默认运行数据提供模块演示
|
||||
data_provider_demo()
|
||||
|
||||
|
||||
def print_usage():
|
||||
"""打印使用帮助"""
|
||||
print("使用方法: python main.py [命令]")
|
||||
print("命令:")
|
||||
print(" data 运行数据提供模块演示")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
requests>=2.25.0
|
||||
pandas>=1.3.0
|
||||
pyyaml>=6.0
|
||||
pytest>=7.0.0
|
3
src/__init__.py
Normal file
3
src/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
云顶之弈阵容推荐器
|
||||
"""
|
4
src/data_provider/__init__.py
Normal file
4
src/data_provider/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .data_loader import DataLoader
|
||||
from .data_query_api import DataQueryAPI
|
||||
|
||||
__all__ = ['DataLoader', 'DataQueryAPI']
|
181
src/data_provider/data_loader.py
Normal file
181
src/data_provider/data_loader.py
Normal file
@ -0,0 +1,181 @@
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
from typing import Dict, List, Optional, Any, Union
|
||||
import logging
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("TFT-Strategist-DataLoader")
|
||||
|
||||
class DataLoader:
|
||||
"""
|
||||
数据加载器,负责从本地文件或在线接口获取云顶之弈的职业、特质和棋子数据
|
||||
"""
|
||||
# 数据源URL
|
||||
DATA_URLS = {
|
||||
'chess': 'https://game.gtimg.cn/images/lol/act/img/tft/js/chess.js',
|
||||
'job': 'https://game.gtimg.cn/images/lol/act/img/tft/js/job.js',
|
||||
'race': 'https://game.gtimg.cn/images/lol/act/img/tft/js/race.js'
|
||||
}
|
||||
|
||||
# 本地数据文件路径
|
||||
DATA_FILES = {
|
||||
'chess': 'data/chess.json',
|
||||
'job': 'data/job.json',
|
||||
'race': 'data/race.json'
|
||||
}
|
||||
|
||||
def __init__(self, use_local_if_exists: bool = True, update_interval: int = 86400):
|
||||
"""
|
||||
初始化数据加载器
|
||||
|
||||
Args:
|
||||
use_local_if_exists: 是否优先使用本地文件(如果存在且未过期)
|
||||
update_interval: 数据更新间隔(秒),默认为24小时
|
||||
"""
|
||||
self.use_local_if_exists = use_local_if_exists
|
||||
self.update_interval = update_interval
|
||||
self.data_cache = {
|
||||
'chess': None,
|
||||
'job': None,
|
||||
'race': None
|
||||
}
|
||||
|
||||
# 确保数据目录存在
|
||||
os.makedirs('data', exist_ok=True)
|
||||
|
||||
def load_all_data(self) -> bool:
|
||||
"""
|
||||
加载所有云顶之弈数据
|
||||
|
||||
Returns:
|
||||
bool: 加载是否成功
|
||||
"""
|
||||
success = True
|
||||
for data_type in self.DATA_URLS.keys():
|
||||
if not self.load_data(data_type):
|
||||
success = False
|
||||
return success
|
||||
|
||||
def load_data(self, data_type: str) -> bool:
|
||||
"""
|
||||
加载指定类型的数据
|
||||
|
||||
Args:
|
||||
data_type: 数据类型,可选值为 'chess', 'job', 'race'
|
||||
|
||||
Returns:
|
||||
bool: 加载是否成功
|
||||
"""
|
||||
if data_type not in self.DATA_URLS:
|
||||
logger.error(f"未知的数据类型: {data_type}")
|
||||
return False
|
||||
|
||||
# 检查本地文件是否存在且未过期
|
||||
local_file = self.DATA_FILES[data_type]
|
||||
if self.use_local_if_exists and os.path.exists(local_file):
|
||||
# 检查文件修改时间
|
||||
file_mtime = os.path.getmtime(local_file)
|
||||
if time.time() - file_mtime < self.update_interval:
|
||||
try:
|
||||
with open(local_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
self.data_cache[data_type] = data
|
||||
logger.info(f"已从本地文件加载 {data_type} 数据")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"从本地加载 {data_type} 数据失败: {e}")
|
||||
|
||||
# 从在线接口获取数据
|
||||
try:
|
||||
logger.info(f"正在从在线接口获取 {data_type} 数据...")
|
||||
response = requests.get(self.DATA_URLS[data_type], timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
|
||||
# 保存到本地文件
|
||||
with open(local_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
self.data_cache[data_type] = data
|
||||
logger.info(f"已从在线接口获取并保存 {data_type} 数据")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"从在线接口获取 {data_type} 数据失败: {e}")
|
||||
return False
|
||||
|
||||
def get_data(self, data_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取指定类型的数据
|
||||
|
||||
Args:
|
||||
data_type: 数据类型,可选值为 'chess', 'job', 'race'
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 数据字典,加载失败时返回None
|
||||
"""
|
||||
if data_type not in self.DATA_URLS:
|
||||
logger.error(f"未知的数据类型: {data_type}")
|
||||
return None
|
||||
|
||||
# 如果数据尚未加载,则先加载
|
||||
if self.data_cache[data_type] is None:
|
||||
if not self.load_data(data_type):
|
||||
return None
|
||||
|
||||
return self.data_cache[data_type]
|
||||
|
||||
def reload_data(self, force: bool = False) -> bool:
|
||||
"""
|
||||
重新加载所有数据
|
||||
|
||||
Args:
|
||||
force: 是否强制从在线接口获取,忽略本地文件
|
||||
|
||||
Returns:
|
||||
bool: 重新加载是否成功
|
||||
"""
|
||||
prev_use_local = self.use_local_if_exists
|
||||
if force:
|
||||
self.use_local_if_exists = False
|
||||
|
||||
success = self.load_all_data()
|
||||
|
||||
# 恢复原来的设置
|
||||
self.use_local_if_exists = prev_use_local
|
||||
|
||||
return success
|
||||
|
||||
def get_latest_version(self) -> Optional[str]:
|
||||
"""
|
||||
获取当前数据的版本号
|
||||
|
||||
Returns:
|
||||
Optional[str]: 版本号,数据未加载时返回None
|
||||
"""
|
||||
# 优先使用棋子数据的版本,如果没有则尝试其他数据源
|
||||
for data_type in ['chess', 'job', 'race']:
|
||||
data = self.get_data(data_type)
|
||||
if data and 'version' in data:
|
||||
return data['version']
|
||||
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
loader = DataLoader()
|
||||
if loader.load_all_data():
|
||||
print(f"数据加载成功,当前版本: {loader.get_latest_version()}")
|
||||
print(f"棋子数量: {len(loader.get_data('chess')['data'])}")
|
||||
print(f"职业数量: {len(loader.get_data('job')['data'])}")
|
||||
print(f"特质数量: {len(loader.get_data('race')['data'])}")
|
||||
else:
|
||||
print("数据加载失败")
|
460
src/data_provider/data_query_api.py
Normal file
460
src/data_provider/data_query_api.py
Normal file
@ -0,0 +1,460 @@
|
||||
from typing import Dict, List, Optional, Any, Set, Tuple, Union
|
||||
import logging
|
||||
from .data_loader import DataLoader
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("TFT-Strategist-DataQueryAPI")
|
||||
|
||||
class DataQueryAPI:
|
||||
"""
|
||||
数据查询API,提供对游戏数据的各种查询功能
|
||||
"""
|
||||
|
||||
def __init__(self, data_loader: Optional[DataLoader] = None):
|
||||
"""
|
||||
初始化数据查询API
|
||||
|
||||
Args:
|
||||
data_loader: 数据加载器实例,如果为None则创建一个新的实例
|
||||
"""
|
||||
self.data_loader = data_loader if data_loader else DataLoader()
|
||||
self._job_cache = {} # 职业缓存
|
||||
self._race_cache = {} # 特质缓存
|
||||
self._chess_cache = {} # 棋子缓存
|
||||
self._job_chess_map = {} # 职业->棋子映射
|
||||
self._race_chess_map = {} # 特质->棋子映射
|
||||
self._chess_job_map = {} # 棋子->职业映射
|
||||
self._chess_race_map = {} # 棋子->特质映射
|
||||
|
||||
# 初始化数据
|
||||
self._init_data()
|
||||
|
||||
def _init_data(self) -> bool:
|
||||
"""
|
||||
初始化并处理数据
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
if not self.data_loader.load_all_data():
|
||||
logger.error("数据加载失败,无法初始化数据查询API")
|
||||
return False
|
||||
|
||||
# 处理职业数据
|
||||
job_data = self.data_loader.get_data('job')
|
||||
if job_data:
|
||||
for job in job_data['data']:
|
||||
job_id = job['jobId']
|
||||
self._job_cache[job_id] = job
|
||||
self._job_chess_map[job_id] = []
|
||||
|
||||
# 处理特质数据
|
||||
race_data = self.data_loader.get_data('race')
|
||||
if race_data:
|
||||
for race in race_data['data']:
|
||||
race_id = race['raceId']
|
||||
self._race_cache[race_id] = race
|
||||
self._race_chess_map[race_id] = []
|
||||
|
||||
# 处理棋子数据并建立关联
|
||||
chess_data = self.data_loader.get_data('chess')
|
||||
if chess_data:
|
||||
for chess in chess_data['data']:
|
||||
chess_id = chess['chessId']
|
||||
self._chess_cache[chess_id] = chess
|
||||
|
||||
# 建立棋子与职业的关联
|
||||
job_ids = chess['jobIds'].split(',') if chess['jobIds'] else []
|
||||
self._chess_job_map[chess_id] = job_ids
|
||||
for job_id in job_ids:
|
||||
if job_id and job_id in self._job_chess_map:
|
||||
self._job_chess_map[job_id].append(chess_id)
|
||||
|
||||
# 建立棋子与特质的关联
|
||||
race_ids = chess['raceIds'].split(',') if chess['raceIds'] else []
|
||||
self._chess_race_map[chess_id] = race_ids
|
||||
for race_id in race_ids:
|
||||
if race_id and race_id in self._race_chess_map:
|
||||
self._race_chess_map[race_id].append(chess_id)
|
||||
|
||||
logger.info(f"数据初始化完成: {len(self._chess_cache)}个棋子, {len(self._job_cache)}个职业, {len(self._race_cache)}个特质")
|
||||
return True
|
||||
|
||||
def reload_data(self, force: bool = False) -> bool:
|
||||
"""
|
||||
重新加载数据
|
||||
|
||||
Args:
|
||||
force: 是否强制从在线接口获取
|
||||
|
||||
Returns:
|
||||
bool: 重新加载是否成功
|
||||
"""
|
||||
if self.data_loader.reload_data(force=force):
|
||||
# 清空缓存
|
||||
self._job_cache.clear()
|
||||
self._race_cache.clear()
|
||||
self._chess_cache.clear()
|
||||
self._job_chess_map.clear()
|
||||
self._race_chess_map.clear()
|
||||
self._chess_job_map.clear()
|
||||
self._chess_race_map.clear()
|
||||
|
||||
# 重新初始化数据
|
||||
return self._init_data()
|
||||
return False
|
||||
|
||||
def get_all_jobs(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取所有职业数据
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 职业数据列表
|
||||
"""
|
||||
return list(self._job_cache.values())
|
||||
|
||||
def get_all_races(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取所有特质数据
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 特质数据列表
|
||||
"""
|
||||
return list(self._race_cache.values())
|
||||
|
||||
def get_all_chess(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取所有棋子数据
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 棋子数据列表
|
||||
"""
|
||||
return list(self._chess_cache.values())
|
||||
|
||||
def get_job_by_id(self, job_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据ID获取职业数据
|
||||
|
||||
Args:
|
||||
job_id: 职业ID
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 职业数据,如果不存在则返回None
|
||||
"""
|
||||
return self._job_cache.get(job_id)
|
||||
|
||||
def get_race_by_id(self, race_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据ID获取特质数据
|
||||
|
||||
Args:
|
||||
race_id: 特质ID
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 特质数据,如果不存在则返回None
|
||||
"""
|
||||
return self._race_cache.get(race_id)
|
||||
|
||||
def get_chess_by_id(self, chess_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据ID获取棋子数据
|
||||
|
||||
Args:
|
||||
chess_id: 棋子ID
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 棋子数据,如果不存在则返回None
|
||||
"""
|
||||
return self._chess_cache.get(chess_id)
|
||||
|
||||
def get_job_by_name(self, name: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据名称获取职业数据
|
||||
|
||||
Args:
|
||||
name: 职业名称
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 职业数据,如果不存在则返回None
|
||||
"""
|
||||
for job in self._job_cache.values():
|
||||
if job['name'] == name:
|
||||
return job
|
||||
return None
|
||||
|
||||
def get_race_by_name(self, name: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据名称获取特质数据
|
||||
|
||||
Args:
|
||||
name: 特质名称
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 特质数据,如果不存在则返回None
|
||||
"""
|
||||
for race in self._race_cache.values():
|
||||
if race['name'] == name:
|
||||
return race
|
||||
return None
|
||||
|
||||
def get_chess_by_name(self, name: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据名称获取棋子数据
|
||||
|
||||
Args:
|
||||
name: 棋子名称(displayName)
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 棋子数据,如果不存在则返回None
|
||||
"""
|
||||
for chess in self._chess_cache.values():
|
||||
if chess.get('displayName') == name:
|
||||
return chess
|
||||
return None
|
||||
|
||||
def get_chess_by_job(self, job_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取指定职业的所有棋子
|
||||
|
||||
Args:
|
||||
job_id: 职业ID
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 棋子数据列表
|
||||
"""
|
||||
if job_id not in self._job_chess_map:
|
||||
return []
|
||||
|
||||
result = []
|
||||
for chess_id in self._job_chess_map[job_id]:
|
||||
chess = self._chess_cache.get(chess_id)
|
||||
if chess:
|
||||
result.append(chess)
|
||||
|
||||
return result
|
||||
|
||||
def get_chess_by_race(self, race_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取指定特质的所有棋子
|
||||
|
||||
Args:
|
||||
race_id: 特质ID
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 棋子数据列表
|
||||
"""
|
||||
if race_id not in self._race_chess_map:
|
||||
return []
|
||||
|
||||
result = []
|
||||
for chess_id in self._race_chess_map[race_id]:
|
||||
chess = self._chess_cache.get(chess_id)
|
||||
if chess:
|
||||
result.append(chess)
|
||||
|
||||
return result
|
||||
|
||||
def get_jobs_of_chess(self, chess_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取指定棋子的所有职业
|
||||
|
||||
Args:
|
||||
chess_id: 棋子ID
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 职业数据列表
|
||||
"""
|
||||
if chess_id not in self._chess_job_map:
|
||||
return []
|
||||
|
||||
result = []
|
||||
for job_id in self._chess_job_map[chess_id]:
|
||||
job = self._job_cache.get(job_id)
|
||||
if job:
|
||||
result.append(job)
|
||||
|
||||
return result
|
||||
|
||||
def get_races_of_chess(self, chess_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取指定棋子的所有特质
|
||||
|
||||
Args:
|
||||
chess_id: 棋子ID
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 特质数据列表
|
||||
"""
|
||||
if chess_id not in self._chess_race_map:
|
||||
return []
|
||||
|
||||
result = []
|
||||
for race_id in self._chess_race_map[chess_id]:
|
||||
race = self._race_cache.get(race_id)
|
||||
if race:
|
||||
result.append(race)
|
||||
|
||||
return result
|
||||
|
||||
def get_all_synergies(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取所有羁绊(职业和特质)数据
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 羁绊数据列表
|
||||
"""
|
||||
return self.get_all_jobs() + self.get_all_races()
|
||||
|
||||
def get_synergy_by_id(self, synergy_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据ID获取羁绊数据
|
||||
|
||||
Args:
|
||||
synergy_id: 羁绊ID
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 羁绊数据,如果不存在则返回None
|
||||
"""
|
||||
# 先在职业中查找
|
||||
job = self.get_job_by_id(synergy_id)
|
||||
if job:
|
||||
return job
|
||||
|
||||
# 再在特质中查找
|
||||
return self.get_race_by_id(synergy_id)
|
||||
|
||||
def get_synergy_by_name(self, name: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据名称获取羁绊数据
|
||||
|
||||
Args:
|
||||
name: 羁绊名称
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 羁绊数据,如果不存在则返回None
|
||||
"""
|
||||
# 先在职业中查找
|
||||
job = self.get_job_by_name(name)
|
||||
if job:
|
||||
return job
|
||||
|
||||
# 再在特质中查找
|
||||
return self.get_race_by_name(name)
|
||||
|
||||
def get_chess_by_synergy(self, synergy_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取指定羁绊的所有棋子
|
||||
|
||||
Args:
|
||||
synergy_id: 羁绊ID
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 棋子数据列表
|
||||
"""
|
||||
# 先在职业中查找
|
||||
chess_list = self.get_chess_by_job(synergy_id)
|
||||
if chess_list:
|
||||
return chess_list
|
||||
|
||||
# 再在特质中查找
|
||||
return self.get_chess_by_race(synergy_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.get_jobs_of_chess(chess_id) + self.get_races_of_chess(chess_id)
|
||||
|
||||
def get_chess_cost_distribution(self) -> Dict[str, int]:
|
||||
"""
|
||||
获取棋子费用分布
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: 费用分布统计,键为费用,值为数量
|
||||
"""
|
||||
result = {}
|
||||
for chess in self._chess_cache.values():
|
||||
cost = chess.get('price', '0')
|
||||
if cost not in result:
|
||||
result[cost] = 0
|
||||
result[cost] += 1
|
||||
|
||||
return result
|
||||
|
||||
def get_chess_by_cost(self, cost: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取指定费用的所有棋子
|
||||
|
||||
Args:
|
||||
cost: 棋子费用
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 棋子数据列表
|
||||
"""
|
||||
result = []
|
||||
for chess in self._chess_cache.values():
|
||||
if chess.get('price') == cost:
|
||||
result.append(chess)
|
||||
|
||||
return result
|
||||
|
||||
def get_synergy_levels(self, synergy_id: str) -> Dict[str, str]:
|
||||
"""
|
||||
获取指定羁绊的等级信息
|
||||
|
||||
Args:
|
||||
synergy_id: 羁绊ID
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 羁绊等级信息,键为等级,值为效果描述
|
||||
"""
|
||||
synergy = self.get_synergy_by_id(synergy_id)
|
||||
if not synergy:
|
||||
return {}
|
||||
|
||||
return synergy.get('level', {})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
api = DataQueryAPI()
|
||||
|
||||
# 获取所有职业和特质
|
||||
print(f"职业数量: {len(api.get_all_jobs())}")
|
||||
print(f"特质数量: {len(api.get_all_races())}")
|
||||
print(f"棋子数量: {len(api.get_all_chess())}")
|
||||
|
||||
# 获取特定羁绊的棋子
|
||||
heavy_warriors = api.get_job_by_name("重装战士")
|
||||
if heavy_warriors:
|
||||
heavy_warriors_id = heavy_warriors['jobId']
|
||||
chess_list = api.get_chess_by_job(heavy_warriors_id)
|
||||
print(f"\n重装战士棋子 ({len(chess_list)}个):")
|
||||
for chess in chess_list:
|
||||
print(f" - {chess['displayName']} (费用: {chess['price']})")
|
||||
|
||||
# 获取某个棋子的所有羁绊
|
||||
some_chess = api.get_all_chess()[0]
|
||||
if some_chess:
|
||||
chess_id = some_chess['chessId']
|
||||
synergies = api.get_synergies_of_chess(chess_id)
|
||||
print(f"\n{some_chess['displayName']}的羁绊:")
|
||||
for synergy in synergies:
|
||||
print(f" - {synergy['name']}")
|
||||
|
||||
# 获取费用分布
|
||||
cost_distribution = api.get_chess_cost_distribution()
|
||||
print("\n棋子费用分布:")
|
||||
for cost, count in sorted(cost_distribution.items()):
|
||||
if cost != '0': # 排除费用为0的棋子(通常是召唤物)
|
||||
print(f" {cost}费棋子: {count}个")
|
110
src/data_provider_demo.py
Normal file
110
src/data_provider_demo.py
Normal file
@ -0,0 +1,110 @@
|
||||
"""
|
||||
数据提供模块演示脚本
|
||||
"""
|
||||
from .data_provider import DataLoader, DataQueryAPI
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("=== 云顶之弈数据提供模块演示 ===")
|
||||
|
||||
# 初始化数据查询API
|
||||
print("\n正在加载数据...")
|
||||
api = DataQueryAPI()
|
||||
|
||||
# 显示数据基本信息
|
||||
print(f"\n当前游戏版本: {api.data_loader.get_latest_version()}")
|
||||
print(f"职业数量: {len(api.get_all_jobs())}")
|
||||
print(f"特质数量: {len(api.get_all_races())}")
|
||||
print(f"棋子数量: {len(api.get_all_chess())}")
|
||||
|
||||
# 显示所有羁绊(职业和特质)
|
||||
print("\n=== 所有羁绊 ===")
|
||||
all_jobs = api.get_all_jobs()
|
||||
all_races = api.get_all_races()
|
||||
|
||||
print("\n职业羁绊:")
|
||||
for job in all_jobs:
|
||||
job_id = job['jobId']
|
||||
chess_count = len(api.get_chess_by_job(job_id))
|
||||
level_info = ' / '.join([f"{level}人:{effect}" for level, effect in job.get('level', {}).items()])
|
||||
print(f" - {job['name']} ({chess_count}个棋子): {level_info}")
|
||||
|
||||
print("\n特质羁绊:")
|
||||
for race in all_races:
|
||||
race_id = race['raceId']
|
||||
chess_count = len(api.get_chess_by_race(race_id))
|
||||
level_info = ' / '.join([f"{level}人:{effect}" for level, effect in race.get('level', {}).items()])
|
||||
print(f" - {race['name']} ({chess_count}个棋子): {level_info}")
|
||||
|
||||
# 显示棋子费用分布
|
||||
cost_distribution = api.get_chess_cost_distribution()
|
||||
print("\n=== 棋子费用分布 ===")
|
||||
for cost, count in sorted(cost_distribution.items()):
|
||||
if cost != '0': # 排除费用为0的棋子(通常是召唤物)
|
||||
print(f" {cost}费棋子: {count}个")
|
||||
|
||||
# 互动查询示例
|
||||
print("\n=== 互动查询 ===")
|
||||
while True:
|
||||
print("\n请选择查询类型:")
|
||||
print("1. 查询羁绊下的所有棋子")
|
||||
print("2. 查询棋子的所有羁绊")
|
||||
print("3. 查询羁绊等级信息")
|
||||
print("4. 退出")
|
||||
|
||||
choice = input("请输入选项 (1-4): ")
|
||||
|
||||
if choice == '1':
|
||||
name = input("请输入羁绊名称: ")
|
||||
synergy = api.get_synergy_by_name(name)
|
||||
if not synergy:
|
||||
print(f"未找到名为 '{name}' 的羁绊")
|
||||
continue
|
||||
|
||||
synergy_id = synergy.get('jobId') or synergy.get('raceId')
|
||||
chess_list = api.get_chess_by_synergy(synergy_id)
|
||||
|
||||
print(f"\n{name} 羁绊的棋子 ({len(chess_list)}个):")
|
||||
for chess in sorted(chess_list, key=lambda x: x.get('price', '0')):
|
||||
price = chess.get('price', '未知')
|
||||
print(f" - {chess['displayName']} ({price}费)")
|
||||
|
||||
elif choice == '2':
|
||||
name = input("请输入棋子名称: ")
|
||||
chess = api.get_chess_by_name(name)
|
||||
if not chess:
|
||||
print(f"未找到名为 '{name}' 的棋子")
|
||||
continue
|
||||
|
||||
chess_id = chess['chessId']
|
||||
synergies = api.get_synergies_of_chess(chess_id)
|
||||
|
||||
print(f"\n{name} 的羁绊 ({len(synergies)}个):")
|
||||
for synergy in synergies:
|
||||
synergy_type = "职业" if 'jobId' in synergy else "特质"
|
||||
print(f" - {synergy['name']} ({synergy_type})")
|
||||
|
||||
elif choice == '3':
|
||||
name = input("请输入羁绊名称: ")
|
||||
synergy = api.get_synergy_by_name(name)
|
||||
if not synergy:
|
||||
print(f"未找到名为 '{name}' 的羁绊")
|
||||
continue
|
||||
|
||||
synergy_id = synergy.get('jobId') or synergy.get('raceId')
|
||||
levels = api.get_synergy_levels(synergy_id)
|
||||
|
||||
print(f"\n{name} 的羁绊等级效果:")
|
||||
for level, effect in sorted(levels.items(), key=lambda x: int(x[0])):
|
||||
print(f" - {level}人: {effect}")
|
||||
|
||||
elif choice == '4':
|
||||
break
|
||||
|
||||
else:
|
||||
print("无效选项,请重新输入")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
3
src/interface/__init__.py
Normal file
3
src/interface/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
接口模块
|
||||
"""
|
3
src/recommendation/__init__.py
Normal file
3
src/recommendation/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
阵容推荐模块
|
||||
"""
|
3
src/scoring/__init__.py
Normal file
3
src/scoring/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
阵容评分模块
|
||||
"""
|
3
tests/__init__.py
Normal file
3
tests/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
测试模块
|
||||
"""
|
190
tests/test_data_provider.py
Normal file
190
tests/test_data_provider.py
Normal file
@ -0,0 +1,190 @@
|
||||
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.data_provider import DataLoader, DataQueryAPI
|
||||
|
||||
|
||||
class TestDataLoader(unittest.TestCase):
|
||||
"""测试DataLoader类"""
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_load_data_from_online(self, mock_get):
|
||||
"""测试从网络加载数据"""
|
||||
# 模拟requests.get返回值
|
||||
mock_response = mock.Mock()
|
||||
mock_response.json.return_value = {
|
||||
"version": "15.7",
|
||||
"data": [{"id": "test"}]
|
||||
}
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# 创建临时数据目录
|
||||
os.makedirs('data', exist_ok=True)
|
||||
|
||||
# 执行测试
|
||||
loader = DataLoader(use_local_if_exists=False)
|
||||
result = loader.load_data('chess')
|
||||
|
||||
# 验证结果
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(loader.get_data('chess')['version'], '15.7')
|
||||
self.assertEqual(len(loader.get_data('chess')['data']), 1)
|
||||
|
||||
# 清理
|
||||
if os.path.exists('data/chess.json'):
|
||||
os.remove('data/chess.json')
|
||||
|
||||
def test_get_latest_version(self):
|
||||
"""测试获取数据版本"""
|
||||
loader = DataLoader()
|
||||
|
||||
# 模拟缓存数据
|
||||
loader.data_cache = {
|
||||
'chess': {"version": "15.7", "data": []},
|
||||
'job': {"version": "15.7", "data": []},
|
||||
'race': {"version": "15.7", "data": []}
|
||||
}
|
||||
|
||||
# 执行测试
|
||||
version = loader.get_latest_version()
|
||||
|
||||
# 验证结果
|
||||
self.assertEqual(version, '15.7')
|
||||
|
||||
|
||||
class TestDataQueryAPI(unittest.TestCase):
|
||||
"""测试DataQueryAPI类"""
|
||||
|
||||
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)
|
||||
|
||||
def test_get_chess_by_job(self):
|
||||
"""测试根据职业获取棋子"""
|
||||
# 创建API实例
|
||||
api = DataQueryAPI(data_loader=self.mock_loader)
|
||||
|
||||
# 手动设置缓存和映射(模拟_init_data方法)
|
||||
api._job_cache = {"10157": self.job_data["data"][0]}
|
||||
api._chess_cache = {
|
||||
"10275": self.chess_data["data"][0],
|
||||
"10276": self.chess_data["data"][1]
|
||||
}
|
||||
api._job_chess_map = {"10157": ["10276"]}
|
||||
|
||||
# 执行测试
|
||||
chess_list = api.get_chess_by_job("10157")
|
||||
|
||||
# 验证结果
|
||||
self.assertEqual(len(chess_list), 1)
|
||||
self.assertEqual(chess_list[0]["displayName"], "阿利斯塔")
|
||||
|
||||
def test_get_chess_by_race(self):
|
||||
"""测试根据特质获取棋子"""
|
||||
# 创建API实例
|
||||
api = DataQueryAPI(data_loader=self.mock_loader)
|
||||
|
||||
# 手动设置缓存和映射
|
||||
api._race_cache = {"10154": self.race_data["data"][0]}
|
||||
api._chess_cache = {
|
||||
"10275": self.chess_data["data"][0],
|
||||
"10276": self.chess_data["data"][1]
|
||||
}
|
||||
api._race_chess_map = {"10154": ["10275"]}
|
||||
|
||||
# 执行测试
|
||||
chess_list = api.get_chess_by_race("10154")
|
||||
|
||||
# 验证结果
|
||||
self.assertEqual(len(chess_list), 1)
|
||||
self.assertEqual(chess_list[0]["displayName"], "布兰德")
|
||||
|
||||
def test_get_synergy_levels(self):
|
||||
"""测试获取羁绊等级信息"""
|
||||
# 创建API实例
|
||||
api = DataQueryAPI(data_loader=self.mock_loader)
|
||||
|
||||
# 手动设置缓存
|
||||
api._job_cache = {"10157": self.job_data["data"][0]}
|
||||
api._race_cache = {"10154": self.race_data["data"][0]}
|
||||
|
||||
# 执行测试
|
||||
job_levels = api.get_synergy_levels("10157")
|
||||
race_levels = api.get_synergy_levels("10154")
|
||||
|
||||
# 验证结果
|
||||
self.assertEqual(len(job_levels), 2)
|
||||
self.assertEqual(job_levels["2"], "16%最大生命值")
|
||||
self.assertEqual(job_levels["4"], "32%最大生命值")
|
||||
|
||||
self.assertEqual(len(race_levels), 2)
|
||||
self.assertEqual(race_levels["3"], "+6% 生命值")
|
||||
self.assertEqual(race_levels["5"], "+10% 生命值")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user