Spaces:
Running
Running
File size: 3,148 Bytes
8538f10 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | """
decklist.py
Parses a pasted decklist into (name, quantity) entries. Accepts the plain
text formats players actually paste, rather than one strict format:
Sol Ring
1 Sol Ring
1x Sol Ring
1 Sol Ring (CMM) 410 <- Moxfield / MTG Arena export
1 Sol Ring (CMM) 410 *F* <- foil / etched markers
1 Sol Ring [CMM] <- bracketed set code
1x Sol Ring (cmm) 410 [Ramp] <- Archidekt export with a category tag
Section headers ("Commander", "Deck:", "// Creatures", ...) are skipped.
Anything after a sideboard/maybeboard/considering header is ignored, since
those cards aren't part of the deck being completed.
Pure text handling only: resolving names against the card vocabulary
happens in server/deckbuilder.py.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
_QTY_RE = re.compile(r"^(\d+)\s*x?\s+(.+)$", re.IGNORECASE)
# Trailing "(SET) 123", "(SET)", "[SET]", "*F*"/"*E*" markers and Archidekt
# "[Category]" / "^Tag^" annotations, stripped repeatedly from the right.
_TRAILING_NOISE_RE = re.compile(
r"\s+(?:\([A-Za-z0-9]{2,6}\)(?:\s+[A-Za-z0-9\-★]+)?|\[[^\]]*\]|\*[A-Za-z]\*|\^[^^]*\^)\s*$"
)
_IGNORED_SECTION_RE = re.compile(
r"^(sideboard|maybeboard|maybe|considering|tokens?)\s*(\(\d+\))?\s*:?$", re.IGNORECASE
)
_HEADER_RE = re.compile(
r"^(commanders?|companion|deck|main\s*deck|mainboard|"
r"creatures?|instants?|sorcery|sorceries|artifacts?|enchantments?|planeswalkers?|"
r"battles?|lands?|other|spells?)\s*(\(\d+\))?\s*:?$",
re.IGNORECASE,
)
@dataclass(frozen=True)
class DecklistEntry:
name: str
quantity: int
line_number: int # 1-based, for pointing the user at a bad line
def _strip_trailing_noise(text: str) -> str:
while True:
stripped = _TRAILING_NOISE_RE.sub("", text)
if stripped == text:
return text.strip()
text = stripped
def parse_decklist(text: str) -> list[DecklistEntry]:
"""Returns one entry per card line, in input order. Duplicate names are
NOT merged here -- the caller merges after name resolution, since two
differently-spelled lines can resolve to the same card."""
entries: list[DecklistEntry] = []
for line_number, raw in enumerate(text.splitlines(), start=1):
line = raw.strip()
if not line or line.startswith("#"):
continue
# A "// Heading" or "SIDEBOARD:" style line: stop at ignored sections,
# skip everything else that looks like a header.
heading = line.lstrip("/").strip()
if line.startswith("//") or line.endswith(":") or _HEADER_RE.match(heading):
if _IGNORED_SECTION_RE.match(heading):
break
continue
if _IGNORED_SECTION_RE.match(heading):
break
quantity = 1
m = _QTY_RE.match(line)
if m:
quantity = int(m.group(1))
line = m.group(2)
name = _strip_trailing_noise(line)
if name and quantity > 0:
entries.append(DecklistEntry(name=name, quantity=quantity, line_number=line_number))
return entries
|