kards-env/kards_battle/cards/card_loader.py

350 lines
12 KiB
Python
Raw Permalink Normal View History

2025-09-05 17:05:43 +08:00
"""
卡牌YAML加载器
从YAML文件加载卡牌定义并创建卡牌对象
"""
import yaml
from typing import List, Dict, Optional, Set, Any
from pathlib import Path
from ..core.enums import Nation, Rarity
from .card import UnitCard, CardStats
from .card_params import ParameterDefinition
class CardLoadError(Exception):
"""卡牌加载错误"""
pass
class CardLoader:
"""卡牌YAML加载器"""
def __init__(self, cards_dir: Optional[str] = None):
"""
初始化卡牌加载器
Args:
cards_dir: 卡牌文件目录默认为 assets/cards/
"""
if cards_dir is None:
# 从当前文件位置推断assets目录
current_dir = Path(__file__).parent
project_root = current_dir.parent.parent # kards_battle -> kards
self.cards_dir = project_root / "assets" / "cards"
else:
self.cards_dir = Path(cards_dir)
# 缓存模板定义
self._templates = {}
self._templates_loaded = False
def _parse_nation(self, nation_str: str) -> Nation:
"""将字符串转换为Nation枚举"""
nation_map = {
'germany': Nation.GERMANY,
'italy': Nation.ITALY,
'finland': Nation.FINLAND,
'japan': Nation.JAPAN,
'usa': Nation.USA,
'uk': Nation.UK,
'soviet': Nation.SOVIET,
'france': Nation.FRANCE,
'poland': Nation.POLAND,
}
nation_lower = nation_str.lower()
if nation_lower not in nation_map:
raise CardLoadError(f"未知的国家: {nation_str}")
return nation_map[nation_lower]
def _parse_rarity(self, rarity: int) -> Rarity:
"""将整数转换为Rarity枚举"""
rarity_map = {
1: Rarity.ELITE,
2: Rarity.RARE,
3: Rarity.UNCOMMON,
4: Rarity.COMMON
}
if rarity not in rarity_map:
raise CardLoadError(f"无效的稀有度: {rarity}必须是1-4")
return rarity_map[rarity]
def _parse_exile_nations(self, exile_nations: Optional[List[str]]) -> Optional[Set[Nation]]:
"""解析流亡国家列表"""
if not exile_nations:
return None
result = set()
for nation_str in exile_nations:
result.add(self._parse_nation(nation_str))
return result
def _load_yaml_file(self, file_path: Path) -> Dict[str, Any]:
"""加载YAML文件"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
except FileNotFoundError:
raise CardLoadError(f"卡牌文件不存在: {file_path}")
except yaml.YAMLError as e:
raise CardLoadError(f"YAML解析错误 ({file_path}): {e}")
def _parse_card_parameters(self, params_data: Optional[List[Dict[str, Any]]]) -> List[ParameterDefinition]:
"""解析卡牌参数定义"""
if not params_data:
return []
parameters = []
for param_data in params_data:
try:
param_def = ParameterDefinition.from_dict(param_data)
parameters.append(param_def)
except Exception as e:
raise CardLoadError(f"解析参数定义失败: {e}")
return parameters
def _load_templates(self):
"""加载卡牌模板定义"""
if self._templates_loaded:
return
spec_file = self.cards_dir / "cards_spec.yaml"
if not spec_file.exists():
self._templates_loaded = True
return
try:
spec_data = self._load_yaml_file(spec_file)
templates = spec_data.get('card_templates', [])
for template in templates:
if 'id' not in template:
continue
self._templates[template['id']] = template
except Exception as e:
raise CardLoadError(f"Failed to load templates: {e}")
self._templates_loaded = True
def _apply_template(self, card_data: Dict[str, Any]) -> Dict[str, Any]:
"""应用模板到卡牌数据"""
self._load_templates()
template_id = card_data.get('template')
if not template_id:
return card_data
if template_id not in self._templates:
raise CardLoadError(f"Template not found: {template_id}")
template = self._templates[template_id]
# 创建合并后的卡牌数据
merged_data = dict(card_data) # 复制原始数据
# 合并模板字段(卡牌数据优先)
for key, value in template.items():
if key == 'id': # 跳过模板ID
continue
if key not in merged_data:
merged_data[key] = value
elif key == 'params':
# 参数需要特殊合并逻辑
template_params = template.get('params', [])
card_params = card_data.get('params', [])
merged_data['params'] = template_params + card_params
return merged_data
def _create_unit_card(self, card_data: Dict[str, Any]) -> UnitCard:
"""从数据字典创建单位卡"""
# 应用模板
card_data = self._apply_template(card_data)
required_fields = ['id', 'name', 'type', 'cost', 'nation', 'rarity', 'unit_definition_id']
for field in required_fields:
if field not in card_data:
raise CardLoadError(f"单位卡缺少必需字段: {field}")
# 验证卡牌类型
if card_data['type'] != 'unit':
raise CardLoadError(f"期望单位卡,但得到: {card_data['type']}")
# 解析基础属性
nation = self._parse_nation(card_data['nation'])
rarity = self._parse_rarity(card_data['rarity'])
exile_nations = self._parse_exile_nations(card_data.get('exile_nations'))
# 解析参数定义
parameters = self._parse_card_parameters(card_data.get('params'))
# 创建单位卡
card = UnitCard(
name=card_data['name'],
cost=card_data['cost'],
nation=nation,
rarity=rarity,
unit_definition_id=card_data['unit_definition_id'],
exile_nations=exile_nations,
description=card_data.get('description', ''),
card_id=card_data['id'],
parameters=parameters
)
return card
def load_unit_cards_from_file(self, file_path: str) -> List[UnitCard]:
"""
从YAML文件加载单位卡
Args:
file_path: YAML文件路径相对于cards_dir
Returns:
单位卡列表
"""
full_path = self.cards_dir / file_path
data = self._load_yaml_file(full_path)
if 'unit_cards' not in data:
raise CardLoadError(f"YAML文件中找不到 'unit_cards' 节点: {file_path}")
cards = []
for card_data in data['unit_cards']:
try:
card = self._create_unit_card(card_data)
cards.append(card)
except Exception as e:
raise CardLoadError(f"创建卡牌失败 (文件: {file_path}, ID: {card_data.get('id', 'unknown')}): {e}")
return cards
def load_nation_cards(self, nation: Nation) -> List[UnitCard]:
"""
加载指定国家的所有卡牌
Args:
nation: 国家
Returns:
该国家的所有卡牌
"""
nation_file_map = {
Nation.GERMANY: 'germany.yaml',
Nation.ITALY: 'italy.yaml',
Nation.USA: 'usa.yaml',
Nation.UK: 'uk.yaml',
Nation.SOVIET: 'soviet.yaml',
Nation.JAPAN: 'japan.yaml',
Nation.FRANCE: 'france.yaml',
Nation.POLAND: 'poland.yaml',
Nation.FINLAND: 'finland.yaml',
}
if nation not in nation_file_map:
raise CardLoadError(f"不支持的国家: {nation}")
filename = nation_file_map[nation]
# 检查文件是否存在
file_path = self.cards_dir / filename
if not file_path.exists():
# 如果文件不存在,返回空列表(某些国家可能还没有卡牌)
return []
return self.load_unit_cards_from_file(filename)
def load_all_available_cards(self) -> Dict[Nation, List[UnitCard]]:
"""
加载所有可用的卡牌
Returns:
按国家分组的卡牌字典
"""
all_cards = {}
# 遍历所有支持的国家
for nation in Nation:
try:
cards = self.load_nation_cards(nation)
if cards: # 只添加有卡牌的国家
all_cards[nation] = cards
except CardLoadError:
# 如果某个国家的卡牌加载失败,跳过但记录
continue
return all_cards
def validate_card_data(self, card_data: Dict[str, Any]) -> List[str]:
"""
验证卡牌数据的完整性
Args:
card_data: 卡牌数据字典
Returns:
错误信息列表空列表表示验证通过
"""
errors = []
# 检查必需字段
required_fields = ['id', 'name', 'type', 'cost', 'nation', 'rarity']
for field in required_fields:
if field not in card_data:
errors.append(f"缺少必需字段: {field}")
# 检查卡牌类型
if 'type' in card_data:
valid_types = ['unit', 'order', 'countermeasure']
if card_data['type'] not in valid_types:
errors.append(f"无效的卡牌类型: {card_data['type']}")
# 检查费用
if 'cost' in card_data:
if not isinstance(card_data['cost'], int) or card_data['cost'] < 0:
errors.append(f"费用必须是非负整数: {card_data['cost']}")
# 检查稀有度
if 'rarity' in card_data:
if not isinstance(card_data['rarity'], int) or card_data['rarity'] < 1 or card_data['rarity'] > 4:
errors.append(f"稀有度必须是1-4的整数: {card_data['rarity']}")
# 检查国家
if 'nation' in card_data:
try:
self._parse_nation(card_data['nation'])
except CardLoadError as e:
errors.append(str(e))
# 单位卡特殊验证
if card_data.get('type') == 'unit':
if 'unit_definition_id' not in card_data:
errors.append("单位卡必须包含 unit_definition_id 字段")
return errors
# 全局加载器实例
_default_loader = None
def get_card_loader() -> CardLoader:
"""获取默认的卡牌加载器实例"""
global _default_loader
if _default_loader is None:
_default_loader = CardLoader()
return _default_loader
def load_nation_cards(nation: Nation) -> List[UnitCard]:
"""便捷函数:加载指定国家的卡牌"""
return get_card_loader().load_nation_cards(nation)
def load_all_cards() -> Dict[Nation, List[UnitCard]]:
"""便捷函数:加载所有可用卡牌"""
return get_card_loader().load_all_available_cards()