Source code for yasuki_gui.ui.deck_builder.deck_io

import datetime
import re

from yasuki_core.card_art import CustomPrint
from yasuki_core.decklist import parse_deck_yaml
from yasuki_gui.ui.deck_builder.deck_data import card_in_side

YAML_SECTIONS = [
    ("pre_game", "SETUP"),
    ("dynasty", "DYNASTY"),
    ("fate", "FATE"),
]
SECTION_LABEL = {"pre_game": "Pre-Game", "dynasty": "Dynasty", "fate": "Fate"}


def _pluralize(word: str) -> str:
    """Title-case-preserving plural for type subheaders (Holding -> Holdings, Strategy -> Strategies)."""
    return word[:-1] + "ies" if word.endswith("y") else word + "s"


_NEEDS_QUOTE = re.compile(r"[:#\[\]{},&*?|<>=!%@`]")


[docs] def serialize_deck( deck_state, repository, deck_name: str = "", deck_author: str = "", today: str | None = None, ) -> str: """ Serialize a DeckState to the portable YAML decklist format. Each deck section is grouped by card type with ``# Type (n)`` subheaders and counts (comments, skipped on import); name/author/date metadata heads the file. Parameters ---------- deck_state : DeckState Current deck composition. repository : DeckBuilderRepository Card data lookup. deck_name : str Deck name to embed in the file. Default ''. deck_author : str Deck author; omitted from the file when empty. Default ''. today : str, optional ISO date for the ``date:`` line. Defaults to today. Returns ------- yaml : str YAML-formatted decklist. """ if today is None: today = datetime.date.today().isoformat() lines = [f"name: {_quote_value(deck_name)}"] if deck_author: lines.append(f"author: {_quote_value(deck_author)}") lines.append(f"date: {today}") lines.append("") cards_by_id = repository.cards_by_id for section_key, side in YAML_SECTIONS: entries = _collect_side_entries(deck_state, cards_by_id, repository, side) if not entries: continue by_type: dict[str, list] = {} for entry in entries: by_type.setdefault(entry[0], []).append(entry) lines.append(f"{SECTION_LABEL[section_key]}: # ({sum(e[4] for e in entries)})") for i, type_name in enumerate(sorted(by_type)): if i > 0: lines.append("") # blank line between type blocks group = sorted(by_type[type_name], key=lambda e: (e[1], e[2] or "", e[3])) lines.append(f" # {_pluralize(type_name)} ({sum(e[4] for e in group)})") for _type, display_name, set_name, art_suffix, count in group: count_prefix = f"{count}x " if count > 1 else "" set_suffix = f" [{set_name}]" if set_name else "" lines.append(f" - {count_prefix}{display_name}{set_suffix}{art_suffix}") lines.append("") return "\n".join(lines).rstrip() + "\n"
def _collect_side_entries(deck_state, cards_by_id, repository, side): entries = [] for card_id, print_list in deck_state.cards.items(): card = cards_by_id.get(card_id) if not card: continue if not card_in_side(card, side): continue display_name = card.get("extended_title") or card.get("name", card_id) type_name = (card.get("types") or ["Other"])[0] for print_id, count in print_list: prints = repository.get_prints(card_id) print_info = next((p for p in prints if p["print_id"] == print_id), None) if print_info and print_info.get("is_custom"): set_name, art_suffix = _custom_entry_suffix(print_info["recipe"], repository) else: set_name = print_info.get("set_name") if print_info else None art_suffix = "" entries.append((type_name, display_name, set_name, art_suffix, count)) return entries def _custom_entry_suffix(recipe, repository): """Return (recipient set name, ``{art: ...}`` suffix) for a custom-print recipe.""" recipient_set = _set_name_for_print( repository, recipe.recipient_card_id, recipe.recipient_print_id ) donor = repository.get_card(recipe.donor_card_id) or {} donor_name = donor.get("extended_title") or donor.get("name", recipe.donor_card_id) donor_set = _set_name_for_print(repository, recipe.donor_card_id, recipe.donor_print_id) donor_ref = f"{donor_name} [{donor_set}]" if donor_set else donor_name return recipient_set, f" {{art: {donor_ref}}}" def _set_name_for_print(repository, card_id, print_id): for p in repository.get_prints(card_id): if p["print_id"] == print_id and not p.get("is_custom"): return p.get("set_name") return None
[docs] def import_deck_yaml( text: str, repository, ) -> tuple: """ Import a YAML decklist into a DeckState. Resolves card names against the repository's local database. Parameters ---------- text : str YAML decklist content repository : DeckBuilderRepository Card data lookup Returns ------- result : tuple of (DeckState, str, str, list of str) (new deck state, deck name, deck author, list of unresolved card names) """ from yasuki_gui.ui.deck_builder.deck_data import DeckState parsed = parse_deck_yaml(text) cards_by_ext = _build_name_index(repository) state = DeckState() unresolved = [] section_sides = {"pre_game": None, "dynasty": "DYNASTY", "fate": "FATE"} for section, entries in [ ("pre_game", parsed["pre_game"]), ("dynasty", parsed["dynasty"]), ("fate", parsed["fate"]), ]: expected_side = section_sides[section] for entry in entries: card, card_id = _resolve_card(entry["name"], cards_by_ext, expected_side, repository) if not card: unresolved.append(entry["name"]) continue prints = repository.get_prints(card_id) matched_print = None if entry["set_name"]: matched_print = next( (p for p in prints if p["set_name"] == entry["set_name"]), None ) if not matched_print and prints: matched_print = prints[0] if not matched_print: unresolved.append(entry["name"]) continue print_id = matched_print["print_id"] if entry.get("art"): custom_id = _resolve_custom_print( card_id, print_id, entry["art"], cards_by_ext, repository ) if custom_id is not None: print_id = custom_id else: unresolved.append(entry["art"]["name"]) for _ in range(entry["count"]): state = state.add_card(card_id, print_id) return state, parsed["name"], parsed["author"], unresolved
def _resolve_custom_print(recipient_card_id, recipient_print_id, art, cards_by_ext, repository): """Register the art-swap recipe for an ``{art: ...}`` entry; return its id or None if unresolved.""" donor = cards_by_ext.get(art["name"].lower()) if not donor: return None _, donor_card_id = donor donor_prints = repository.get_prints(donor_card_id) donor_print = None if art["set_name"]: donor_print = next((p for p in donor_prints if p["set_name"] == art["set_name"]), None) if not donor_print and donor_prints: donor_print = donor_prints[0] if not donor_print: return None recipe = CustomPrint( recipient_card_id, recipient_print_id, donor_card_id, donor_print["print_id"] ) return repository.register_custom_print(recipe) def _build_name_index(repository) -> dict[str, tuple[dict, str]]: """ Build a case-insensitive name → (card, card_id) index. Keys by extended_title first, then name as fallback (no overwrite). """ index: dict[str, tuple[dict, str]] = {} for card_id, card in repository.cards_by_id.items(): key = (card.get("extended_title") or card.get("name", "")).lower() if key: index[key] = (card, card_id) for card_id, card in repository.cards_by_id.items(): name_key = card.get("name", "").lower() if name_key and name_key not in index: index[name_key] = (card, card_id) return index def _resolve_card(name, cards_by_ext, expected_side, repository): key = name.lower() result = cards_by_ext.get(key) if not result: return None, None return result def _quote_value(s: str) -> str: if _NEEDS_QUOTE.search(s) or s.startswith(" ") or s.endswith(" "): escaped = s.replace("\\", "\\\\").replace('"', '\\"') return f'"{escaped}"' return s