From 874561f42356e18e1e3ee325ddea7cf13de6a4bf Mon Sep 17 00:00:00 2001 From: KoalaTrapLord Date: Tue, 17 Mar 2026 05:14:35 -0400 Subject: [PATCH] Phase 2c: Extract card analysis to services/card_analysis.py (#12) --- routes/shared.py | 88 +-------------------------------------- services/card_analysis.py | 81 +++++++++++++++++++++++++++++++++++ services/scryfall.py | 2 +- 3 files changed, 84 insertions(+), 87 deletions(-) create mode 100644 services/card_analysis.py diff --git a/routes/shared.py b/routes/shared.py index 1a2a413..e664592 100644 --- a/routes/shared.py +++ b/routes/shared.py @@ -146,92 +146,8 @@ from services.database import ( _JSON_FIELDS, VALID_SORT_FIELDS, _row_to_dict, _snake_to_camel, _add_image_url, _build_collection_filters, ) -# Card Role Detection -# ══════════════════════════════════════════════════════════════ - -def _detect_card_roles(oracle_text: str, type_line: str, keywords) -> list: - roles = [] - ot = (oracle_text or "").lower() - tl = (type_line or "").lower() - kw_raw = keywords or [] - if isinstance(kw_raw, str): - try: - kw_raw = json.loads(kw_raw) - except Exception: - kw_raw = [] - kw = " ".join(str(k).lower() for k in kw_raw) - - if ("add {" in ot or "add one mana" in ot - or ("search your library for a" in ot and "land" in ot) - or "treasure token" in ot or "add mana" in ot): - roles.append("Ramp") - if ("draw a card" in ot or "draw cards" in ot or "draws a card" in ot - or "draw two cards" in ot or "draw three cards" in ot - or re.search(r"draw \d+ cards?", ot) - or ("whenever" in ot and "draw" in ot)): - roles.append("Draw") - if ("destroy target" in ot or "exile target" in ot - or re.search(r"deals? \d+ damage to (target|any target)", ot) - or re.search(r"deals? x damage to (target|any target)", ot) - or "target creature gets -" in ot or "fights target" in ot): - roles.append("Removal") - if ("destroy all" in ot or "exile all" in ot - or re.search(r"all creatures get -\d+/-\d+", ot) - or re.search(r"each creature gets -\d+/-\d+", ot) - or ("deals" in ot and "to each creature" in ot)): - roles.append("Board Wipe") - if (re.search(r"creatures? you control get \+", ot) - or re.search(r"creatures? you control have", ot) - or re.search(r"other creatures? you control get \+", ot) - or re.search(r"each creature you control gets? \+", ot)): - roles.append("Anthem") - if ("hexproof" in ot or "hexproof" in kw or "indestructible" in ot - or "indestructible" in kw or "shroud" in ot or "shroud" in kw - or "ward" in ot or "ward" in kw or "protection from" in ot - or "can't be the target" in ot): - roles.append("Protection") - if "search your library" in ot: - roles.append("Tutor") - if "counter target" in ot: - roles.append("Counter") - if re.search(r"create[sd]? .*token", ot) or "creature token" in ot: - roles.append("Token") - if ("sacrifice a " in ot or "sacrifice another" in ot - or ("when" in ot and "sacrifice" in ot and "dies" not in ot) - or "each player sacrifices" in ot): - roles.append("Sacrifice") - if (("return" in ot and "from your graveyard" in ot) - or ("return" in ot and "from a graveyard" in ot) - or ("put" in ot and "from your graveyard" in ot and "onto the battlefield" in ot) - or ("put" in ot and "from a graveyard" in ot and "onto the battlefield" in ot)): - roles.append("Recursion") - if (("graveyard" in ot and ("mill" in ot or ("put" in ot and "into" in ot and "graveyard" in ot))) - or "mill" in kw): - roles.append("Graveyard") - if ("gain" in ot and "life" in ot) or "lifelink" in ot or "lifelink" in kw: - roles.append("Lifegain") - if (re.search(r"deals? \d+ damage to (each|any|target) (opponent|player)", ot) - or ("each opponent loses" in ot and "life" in ot) - or "deals damage to each opponent" in ot): - roles.append("Burn") - if ("can't cast" in ot or "can't attack" in ot or "can't activate" in ot - or ("enters the battlefield tapped" in ot and "opponents" in ot) - or "each player can't" in ot or "players can't" in ot - or ("cost {" in ot and "more to cast" in ot)): - roles.append("Stax") - if ("flying" in kw or "trample" in kw or "menace" in kw or "shadow" in kw - or "fear" in kw or "intimidate" in kw - or "can't be blocked" in ot or "unblockable" in ot): - roles.append("Evasion") - if ("you win the game" in ot or "extra turn" in ot or "infinite" in ot - or "loses the game" in ot - or ("damage equal to" in ot and ("number" in ot or "total" in ot))): - roles.append("Finisher") - if ("untap all" in ot or ("copy" in ot and "spell" in ot) - or "take an extra" in ot - or ("double" in ot and ("damage" in ot or "counters" in ot or "tokens" in ot))): - roles.append("Combo") - return roles +# Card Role Detection (imported from services/card_analysis.py) +from services.card_analysis import _detect_card_roles # ══════════════════════════════════════════════════════════════ diff --git a/services/card_analysis.py b/services/card_analysis.py new file mode 100644 index 0000000..2e9e69b --- /dev/null +++ b/services/card_analysis.py @@ -0,0 +1,81 @@ +"""Card role detection, type classification, and deck composition analysis.""" +import json +import re + + +def _detect_card_roles(oracle_text: str, type_line: str, keywords) -> list: + roles = [] + ot = (oracle_text or "").lower() + tl = (type_line or "").lower() + kw_raw = keywords or [] + if isinstance(kw_raw, str): + try: + kw_raw = json.loads(kw_raw) + except Exception: + kw_raw = [] + kw = " ".join(str(k).lower() for k in kw_raw) + + if ("add {" in ot or "add one mana" in ot + or ("search your library for a" in ot and "land" in ot) + or "treasure token" in ot or "add mana" in ot): + roles.append("Ramp") + if ("draw a card" in ot or "draw cards" in ot or "draws a card" in ot + or "draw two cards" in ot or "draw three cards" in ot + or re.search(r"draw \d+ cards?", ot) + or ("whenever" in ot and "draw" in ot)): + roles.append("Draw") + if ("destroy target" in ot or "exile target" in ot + or re.search(r"deals? \d+ damage to (target|any target)", ot) + or re.search(r"deals? x damage to (target|any target)", ot) + or "target creature gets -" in ot or "fights target" in ot): + roles.append("Removal") + if ("destroy all" in ot or "exile all" in ot + or re.search(r"all creatures get -\d+/-\d+", ot) + or re.search(r"each creature gets -\d+/-\d+", ot) + or ("deals" in ot and "to each creature" in ot)): + roles.append("Board Wipe") + if "search your library" in ot and "land" not in ot: + roles.append("Tutor") + if ("counter target" in ot or "counter that" in ot + or "counters" in kw): + roles.append("Counter") + if ("create" in ot and ("token" in ot or "tokens" in ot)): + roles.append("Token") + if ("each creature you control gets" in ot or "creatures you control get" in ot + or "all creatures you control" in ot or "anthem" in tl.lower()): + roles.append("Anthem") + if ("target creature gains" in ot and ("hexproof" in ot or "indestructible" in ot or "shroud" in ot))\ + or ("protection" in kw): + roles.append("Protection") + if ("sacrifice" in ot and ("whenever" in ot or "you may" in ot)): + roles.append("Sacrifice") + if ("return" in ot and ("from your graveyard" in ot and "onto the battlefield" in ot) + or ("put" in ot and "from a graveyard" in ot and "onto the battlefield" in ot)): + roles.append("Recursion") + if (("graveyard" in ot and ("mill" in ot or "put" in ot and "into" in ot and "graveyard" in ot)) + or "mill" in kw): + roles.append("Graveyard") + if ("gain" in ot and "life" in ot) or "lifelink" in ot or "lifelink" in kw: + roles.append("Lifegain") + if (re.search(r"deals? \d+ damage to (each|any|target) (opponent|player)", ot) + or "each opponent loses" in ot and "life" in ot + or "deals damage to each opponent" in ot): + roles.append("Burn") + if ("can't cast" in ot or "can't attack" in ot or "can't activate" in ot + or ("enters the battlefield tapped" in ot and "opponents" in ot) + or "each player can't" in ot or "players can't" in ot + or ("cost {" in ot and "more to cast" in ot)): + roles.append("Stax") + if ("flying" in kw or "trample" in kw or "menace" in kw or "shadow" in kw + or "fear" in kw or "intimidate" in kw + or "can't be blocked" in ot or "unblockable" in ot): + roles.append("Evasion") + if ("you win the game" in ot or "extra turn" in ot or "infinite" in ot + or "loses the game" in ot + or ("damage equal to" in ot and ("number" in ot or "total" in ot))): + roles.append("Finisher") + if ("untap all" in ot or ("copy" in ot and "spell" in ot) + or "take an extra" in ot + or ("double" in ot and ("damage" in ot or "counters" in ot or "tokens" in ot))): + roles.append("Combo") + return roles \ No newline at end of file diff --git a/services/scryfall.py b/services/scryfall.py index 2ef4418..e905ece 100644 --- a/services/scryfall.py +++ b/services/scryfall.py @@ -11,7 +11,7 @@ from urllib.request import Request, urlopen # Lazy import to avoid circular dependency def _detect_card_roles(oracle_text, type_line, keywords): - from routes.shared import _detect_card_roles as _dcr + from services.card_analysis import _detect_card_roles as _dcr return _dcr(oracle_text, type_line, keywords)