Spaces:
Sleeping
Sleeping
| import json | |
| import os | |
| from typing import Any, Dict, Tuple | |
| from pydantic import TypeAdapter | |
| from engine.models.ability import AbilityCostType, Cost | |
| from engine.models.card import EnergyCard, LiveCard, MemberCard | |
| class CardDataLoader: | |
| def __init__(self, json_path: str): | |
| self.json_path = json_path | |
| def _repair_stale_optional_energy_deck_costs(self, cards: Dict[int, Any]) -> None: | |
| """Normalize legacy compiled abilities that lost PLACE_ENERGY_WAIT costs. | |
| Some older compiled card files encoded | |
| `COST: PLACE_ENERGY_WAIT(1) (Optional)` as an empty-cost ability with only | |
| the follow-up effect preserved. Rehydrate those into a real deck-to-energy | |
| cost so the runtime prompt and resolution path stay consistent. | |
| """ | |
| for card in cards.values(): | |
| for ability in getattr(card, "abilities", []): | |
| text = f"{getattr(ability, 'pseudocode', '')}\n{getattr(ability, 'raw_text', '')}".upper() | |
| if "COST: PLACE_ENERGY_WAIT" not in text: | |
| continue | |
| if getattr(ability, "costs", None): | |
| continue | |
| is_optional = "(OPTIONAL)" in text | |
| ability.costs = [ | |
| Cost( | |
| AbilityCostType.PLACE_ENERGY_FROM_DECK, | |
| 1, | |
| params={"wait": True}, | |
| is_optional=is_optional, | |
| ) | |
| ] | |
| semantic = getattr(ability, "semantic_form", None) | |
| if isinstance(semantic, dict): | |
| semantic["costs"] = [ | |
| { | |
| "type": "PLACE_ENERGY_FROM_DECK", | |
| "value": 1, | |
| "target": "PLAYER", | |
| "params": {"wait": True}, | |
| "optional": is_optional, | |
| } | |
| ] | |
| def load(self) -> Tuple[Dict[int, MemberCard], Dict[int, LiveCard], Dict[int, Any]]: | |
| # Auto-detect compiled file | |
| target_path = self.json_path | |
| if target_path.endswith("cards.json"): | |
| # Check for compiled file in the same directory, or in data/ | |
| compiled_path = target_path.replace("cards.json", "cards_compiled.json") | |
| if os.path.exists(compiled_path): | |
| target_path = compiled_path | |
| else: | |
| root_path = os.path.join(os.getcwd(), "data", "cards_compiled.json") | |
| if os.path.exists(root_path): | |
| target_path = root_path | |
| # Fallback to relative path search if absolute fails (common in tests) | |
| if not os.path.exists(target_path): | |
| # Try assuming path is relative to project root | |
| # But we don't know project root easily. | |
| pass | |
| # print(f"Loading card data from {target_path}...") | |
| with open(target_path, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| members = {} | |
| lives = {} | |
| energy = {} | |
| if "member_db" in data: | |
| # Compiled format (v1.0) | |
| m_adapter = TypeAdapter(MemberCard) | |
| l_adapter = TypeAdapter(LiveCard) | |
| e_adapter = TypeAdapter(EnergyCard) | |
| for k, v in data["member_db"].items(): | |
| members[int(k)] = m_adapter.validate_python(v) | |
| for k, v in data["live_db"].items(): | |
| # print(f"Loading live {k}") | |
| lives[int(k)] = l_adapter.validate_python(v) | |
| # print(f"DEBUG: Internal live_db keys: {len(data['live_db'])}, loaded: {len(lives)}") | |
| for k, v in data["energy_db"].items(): | |
| energy[int(k)] = e_adapter.validate_python(v) | |
| self._repair_stale_optional_energy_deck_costs(lives) | |
| self._repair_stale_optional_energy_deck_costs(members) | |
| else: | |
| # Legacy raw format | |
| # Since we removed runtime parsing from the engine to separate concerns, | |
| # we cannot load raw cards anymore. | |
| raise RuntimeError( | |
| "Legacy cards.json format detected. Runtime parsing is disabled. " | |
| "Please run 'uv run compiler/main.py' to generate 'data/cards_compiled.json'." | |
| ) | |
| return members, lives, energy | |