"""Deck list parsing and serialization utilities.""" import re from typing import List, Dict, Tuple, Optional from dataclasses import dataclass @dataclass class CardInfo: """Card information extracted from deck text.""" count: int name: str set_code: Optional[str] = None collector_number: Optional[str] = None def __str__(self) -> str: if self.set_code and self.collector_number: return f"{self.count} {self.name} ({self.set_code}-{self.collector_number})" return f"{self.count} {self.name}" class DeckParser: """Parse MTG Online deck list formats.""" # Regex patterns for deck parsing CARD_LINE_RE = re.compile(r"^\s*[\w\[\(\{].*$") EMPTY_LINE_RE = re.compile(r"^\s*$") COMMENT_RE = re.compile(r"([\w\[\(\{].*$)") SB_MARK_RE = re.compile(r"^\s*sb:\s*(.+)", re.IGNORECASE) SB_COMMENT_RE = re.compile(r"^sideboard\s.*$", re.IGNORECASE) DECK_COMMENT_RE = re.compile(r"^((main)?deck(list)?|mainboard)\b", re.IGNORECASE) MULTIPLIER_RE = re.compile(r"^([xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))") HYPHEN_FORMAT_RE = re.compile(r"\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*)") REGULAR_FORMAT_RE = re.compile(r"\((\w{3,})\)\s+(\d+[^\w\s]*)") def parse_plain_text(self, text: str) -> List[CardInfo]: """Parse plain text deck format.""" lines = text.strip().split('\n') max_line = len(lines) # Find deck start (first card line) deck_start = -1 for i, line in enumerate(lines): if self.CARD_LINE_RE.match(line): deck_start = i break if deck_start == -1: return [] # Find sideboard start sBStart = -1 for i in range(deck_start, max_line): if self.SB_MARK_RE.match(lines[i]): sBStart = i break elif self.SB_COMMENT_RE.match(lines[i]): sBStart = i break if sBStart == -1: # Look for empty line after deck for i in range(deck_start + 1, max_line): if self.EMPTY_LINE_RE.match(lines[i]): # Check if there are cards after the empty line for j in range(i + 1, max_line): if self.CARD_LINE_RE.match(lines[j]): sBStart = i break if sBStart != -1: break if sBStart == -1: sBStart = max_line cards = [] index = 0 # Skip comments while index < deck_start: if not self.EMPTY_LINE_RE.match(lines[index]): index += 1 else: break # Parse cards for i in range(index, sBStart): line = lines[i].strip() if not line or self.EMPTY_LINE_RE.match(line): continue # Check for sideboard marker if self.SB_MARK_RE.match(line): match = self.SB_MARK_RE.match(line) card_name = match.group(1).strip() if match else line sideboard = True else: card_name = line sideboard = False # Extract set code and collector number set_code = None collector_number = None match = self.HYPHEN_FORMAT_RE.search(card_name) if match: set_code = match.group(2).upper() collector_number = match.group(3) card_name = card_name[:match.start()].strip() else: match = self.REGULAR_FORMAT_RE.search(card_name) if match: set_code = match.group(1).upper() collector_number = match.group(2) card_name = card_name[:match.start()].strip() # Extract count count = 1 match = self.MULTIPLIER_RE.match(card_name) if match: count = int(match.group(2)) card_name = match.group(3) # Normalize card name card_name = card_name.strip() if card_name: cards.append(CardInfo( count=count, name=card_name, set_code=set_code, collector_number=collector_number, )) return cards def format_plain_text(self, cards: List[CardInfo], prefix_sideboard: bool = True) -> str: """Format cards as plain text deck list.""" lines = [] for card in cards: prefix = "SB: " if prefix_sideboard and card.name.startswith("[SB]") else "" name = card.name if card.name.startswith("[SB]"): name = card.name[4:] # Remove [SB] prefix if card.set_code and card.collector_number: lines.append(f"{prefix}{card.count} {name} ({card.set_code}-{card.collector_number})") else: lines.append(f"{prefix}{card.count} {name}") return "\n".join(lines) def to_native_xml(self, cards: List[CardInfo]) -> str: """Convert cards to MTG Online native XML format.""" xml_lines = [''] # Group cards by zone (main/sideboard) main_cards = [c for c in cards if not c.name.startswith("[SB]")] side_cards = [c for c in cards if c.name.startswith("[SB]")] if main_cards: xml_lines.append(' ') for card in main_cards: xml_lines.append(f' ') xml_lines.append(' ') if side_cards: xml_lines.append(' ') for card in side_cards: clean_name = card.name[4:] # Remove [SB] prefix xml_lines.append(f' ') xml_lines.append(' ') xml_lines.append('') return "\n".join(xml_lines) def from_native_xml(self, xml: str) -> List[CardInfo]: """Parse MTG Online native XML format.""" cards = [] # Simple XML parsing (in production, use proper XML parser) card_pattern = re.compile(r'') for match in card_pattern.finditer(xml): name = match.group(1) count = int(match.group(2)) cards.append(CardInfo(count=count, name=name)) return cards # Convenience functions def parse_deck(text: str) -> List[CardInfo]: """Parse a deck list from plain text or native XML.""" parser = DeckParser() if text.strip().startswith(" str: """Format cards to specified deck format.""" parser = DeckParser() if format_type == "native": return parser.to_native_xml(cards) else: return parser.format_plain_text(cards, prefix_sideboard)