from typing import Dict, List, Final, Tuple, Union, Callable, Any from settings import CARD_DATA_COLUMNS as CSV_REQUIRED_COLUMNS # unified from path_util import csv_dir import pandas as pd __all__ = [ 'CSV_REQUIRED_COLUMNS', 'get_commanders', 'get_backgrounds', ] import ast # Commander selection configuration # Format string for displaying duplicate cards in deck lists FUZZY_MATCH_THRESHOLD: Final[int] = 90 # Threshold for fuzzy name matching MAX_FUZZY_CHOICES: Final[int] = 5 # Maximum number of fuzzy match choices # Commander-related constants DUPLICATE_CARD_FORMAT: Final[str] = '{card_name} x {count}' # M4: Deprecated - use Parquet loading instead COMMANDER_CSV_PATH: Final[str] = f"{csv_dir()}/commander_cards.csv" DECK_DIRECTORY = '../deck_files' # M4: Deprecated - Parquet handles types natively (no converters needed) COMMANDER_CONVERTERS: Final[Dict[str, Any]] = { 'themeTags': ast.literal_eval, 'creatureTypes': ast.literal_eval, 'roleTags': ast.literal_eval, } # CSV loading converters COMMANDER_POWER_DEFAULT: Final[int] = 0 COMMANDER_TOUGHNESS_DEFAULT: Final[int] = 0 COMMANDER_MANA_VALUE_DEFAULT: Final[int] = 0 COMMANDER_TYPE_DEFAULT: Final[str] = '' COMMANDER_TEXT_DEFAULT: Final[str] = '' COMMANDER_MANA_COST_DEFAULT: Final[str] = '' COMMANDER_COLOR_IDENTITY_DEFAULT: Final[str] = '' COMMANDER_COLORS_DEFAULT: Final[List[str]] = [] COMMANDER_CREATURE_TYPES_DEFAULT: Final[str] = '' COMMANDER_TAGS_DEFAULT: Final[List[str]] = [] COMMANDER_THEMES_DEFAULT: Final[List[str]] = [] # Reporting / summaries TAG_SUMMARY_MIN_COUNT: Final[int] = 3 # Minimum unique-card count for a tag to appear in tag summary output TAG_SUMMARY_ALWAYS_SHOW_SUBSTRS: Final[List[str]] = [ 'board wipe', # ensure board wipes always shown even if below threshold 'mass removal' # common alternate phrasing ] CARD_TYPES = ['Artifact','Creature', 'Enchantment', 'Instant', 'Land', 'Planeswalker', 'Sorcery', 'Kindred', 'Dungeon', 'Battle'] # Basic mana colors MANA_COLORS: Final[List[str]] = ['W', 'U', 'B', 'R', 'G'] # Mana pip patterns for each color MANA_PIP_PATTERNS: Final[Dict[str, str]] = { color: f'{{{color}}}' for color in MANA_COLORS } MONO_COLOR_MAP: Final[Dict[str, Tuple[str, List[str]]]] = { 'COLORLESS': ('Colorless', ['colorless']), 'W': ('White', ['colorless', 'white']), 'U': ('Blue', ['colorless', 'blue']), 'B': ('Black', ['colorless', 'black']), 'R': ('Red', ['colorless', 'red']), 'G': ('Green', ['colorless', 'green']) } DUAL_COLOR_MAP: Final[Dict[str, Tuple[str, List[str], List[str]]]] = { 'B, G': ('Golgari: Black/Green', ['B', 'G', 'B, G'], ['colorless', 'black', 'green', 'golgari']), 'B, R': ('Rakdos: Black/Red', ['B', 'R', 'B, R'], ['colorless', 'black', 'red', 'rakdos']), 'B, U': ('Dimir: Black/Blue', ['B', 'U', 'B, U'], ['colorless', 'black', 'blue', 'dimir']), 'B, W': ('Orzhov: Black/White', ['B', 'W', 'B, W'], ['colorless', 'black', 'white', 'orzhov']), 'G, R': ('Gruul: Green/Red', ['G', 'R', 'G, R'], ['colorless', 'green', 'red', 'gruul']), 'G, U': ('Simic: Green/Blue', ['G', 'U', 'G, U'], ['colorless', 'green', 'blue', 'simic']), 'G, W': ('Selesnya: Green/White', ['G', 'W', 'G, W'], ['colorless', 'green', 'white', 'selesnya']), 'R, U': ('Izzet: Blue/Red', ['U', 'R', 'U, R'], ['colorless', 'blue', 'red', 'izzet']), 'U, W': ('Azorius: Blue/White', ['U', 'W', 'U, W'], ['colorless', 'blue', 'white', 'azorius']), 'R, W': ('Boros: Red/White', ['R', 'W', 'R, W'], ['colorless', 'red', 'white', 'boros']) } TRI_COLOR_MAP: Final[Dict[str, Tuple[str, List[str], List[str]]]] = { 'B, G, U': ('Sultai: Black/Blue/Green', ['B', 'G', 'U', 'B, G', 'B, U', 'G, U', 'B, G, U'], ['colorless', 'black', 'blue', 'green', 'dimir', 'golgari', 'simic', 'sultai']), 'B, G, R': ('Jund: Black/Red/Green', ['B', 'G', 'R', 'B, G', 'B, R', 'G, R', 'B, G, R'], ['colorless', 'black', 'green', 'red', 'golgari', 'rakdos', 'gruul', 'jund']), 'B, G, W': ('Abzan: Black/Green/White', ['B', 'G', 'W', 'B, G', 'B, W', 'G, W', 'B, G, W'], ['colorless', 'black', 'green', 'white', 'golgari', 'orzhov', 'selesnya', 'abzan']), 'B, R, U': ('Grixis: Black/Blue/Red', ['B', 'R', 'U', 'B, R', 'B, U', 'R, U', 'B, R, U'], ['colorless', 'black', 'blue', 'red', 'dimir', 'rakdos', 'izzet', 'grixis']), 'B, R, W': ('Mardu: Black/Red/White', ['B', 'R', 'W', 'B, R', 'B, W', 'R, W', 'B, R, W'], ['colorless', 'black', 'red', 'white', 'rakdos', 'orzhov', 'boros', 'mardu']), 'B, U, W': ('Esper: Black/Blue/White', ['B', 'U', 'W', 'B, U', 'B, W', 'U, W', 'B, U, W'], ['colorless', 'black', 'blue', 'white', 'dimir', 'orzhov', 'azorius', 'esper']), 'G, R, U': ('Temur: Blue/Green/Red', ['G', 'R', 'U', 'G, R', 'G, U', 'R, U', 'G, R, U'], ['colorless', 'green', 'red', 'blue', 'simic', 'izzet', 'gruul', 'temur']), 'G, R, W': ('Naya: Green/Red/White', ['G', 'R', 'W', 'G, R', 'G, W', 'R, W', 'G, R, W'], ['colorless', 'green', 'red', 'white', 'gruul', 'selesnya', 'boros', 'naya']), 'G, U, W': ('Bant: Blue/Green/White', ['G', 'U', 'W', 'G, U', 'G, W', 'U, W', 'G, U, W'], ['colorless', 'green', 'blue', 'white', 'simic', 'azorius', 'selesnya', 'bant']), 'R, U, W': ('Jeskai: Blue/Red/White', ['R', 'U', 'W', 'R, U', 'U, W', 'R, W', 'R, U, W'], ['colorless', 'blue', 'red', 'white', 'izzet', 'azorius', 'boros', 'jeskai']) } OTHER_COLOR_MAP: Final[Dict[str, Tuple[str, List[str], List[str]]]] = { 'B, G, R, U': ('Glint: Black/Blue/Green/Red', ['B', 'G', 'R', 'U', 'B, G', 'B, R', 'B, U', 'G, R', 'G, U', 'R, U', 'B, G, R', 'B, G, U', 'B, R, U', 'G, R, U', 'B, G, R, U'], ['colorless', 'black', 'blue', 'green', 'red', 'golgari', 'rakdos', 'dimir', 'gruul', 'simic', 'izzet', 'jund', 'sultai', 'grixis', 'temur', 'glint']), 'B, G, R, W': ('Dune: Black/Green/Red/White', ['B', 'G', 'R', 'W', 'B, G', 'B, R', 'B, W', 'G, R', 'G, W', 'R, W', 'B, G, R', 'B, G, W', 'B, R, W', 'G, R, W', 'B, G, R, W'], ['colorless', 'black', 'green', 'red', 'white', 'golgari', 'rakdos', 'orzhov', 'gruul', 'selesnya', 'boros', 'jund', 'abzan', 'mardu', 'naya', 'dune']), 'B, G, U, W': ('Witch: Black/Blue/Green/White', ['B', 'G', 'U', 'W', 'B, G', 'B, U', 'B, W', 'G, U', 'G, W', 'U, W', 'B, G, U', 'B, G, W', 'B, U, W', 'G, U, W', 'B, G, U, W'], ['colorless', 'black', 'blue', 'green', 'white', 'golgari', 'dimir', 'orzhov', 'simic', 'selesnya', 'azorius', 'sultai', 'abzan', 'esper', 'bant', 'witch']), 'B, R, U, W': ('Yore: Black/Blue/Red/White', ['B', 'R', 'U', 'W', 'B, R', 'B, U', 'B, W', 'R, U', 'R, W', 'U, W', 'B, R, U', 'B, R, W', 'B, U, W', 'R, U, W', 'B, R, U, W'], ['colorless', 'black', 'blue', 'red', 'white', 'rakdos', 'dimir', 'orzhov', 'izzet', 'boros', 'azorius', 'grixis', 'mardu', 'esper', 'jeskai', 'yore']), 'G, R, U, W': ('Ink: Blue/Green/Red/White', ['G', 'R', 'U', 'W', 'G, R', 'G, U', 'G, W', 'R, U', 'R, W', 'U, W', 'G, R, U', 'G, R, W', 'G, U, W', 'R, U, W', 'G, R, U, W'], ['colorless', 'blue', 'green', 'red', 'white', 'gruul', 'simic', 'selesnya', 'izzet', 'boros', 'azorius', 'temur', 'naya', 'bant', 'jeskai', 'ink']), 'B, G, R, U, W': ('WUBRG: All colors', ['B', 'G', 'R', 'U', 'W', 'B, G', 'B, R', 'B, U', 'B, W', 'G, R', 'G, U', 'G, W', 'R, U', 'R, W', 'U, W', 'B, G, R', 'B, G, U', 'B, G, W', 'B, R, U', 'B, R, W', 'B, U, W', 'G, R, U', 'G, R, W', 'G, U, W', 'R, U, W', 'B, G, R, U', 'B, G, R, W', 'B, G, U, W', 'B, R, U, W', 'G, R, U, W', 'B, G, R, U, W'], ['colorless', 'black', 'green', 'red', 'blue', 'white', 'golgari', 'rakdos', 'dimir', 'orzhov', 'gruul', 'simic', 'selesnya', 'izzet', 'boros', 'azorius', 'jund', 'sultai', 'abzan', 'grixis', 'mardu', 'esper', 'temur', 'naya', 'bant', 'jeskai', 'glint', 'dune', 'witch', 'yore', 'ink', 'wubrg']) } # Card category validation rules CREATURE_VALIDATION_RULES: Final[Dict[str, Dict[str, Any]]] = { 'power': {'type': ('str', 'int', 'float'), 'required': True}, 'toughness': {'type': ('str', 'int', 'float'), 'required': True}, 'creatureTypes': {'type': 'list', 'required': True} } SPELL_VALIDATION_RULES: Final[Dict[str, Dict[str, Any]]] = { 'manaCost': {'type': 'str', 'required': True}, 'text': {'type': 'str', 'required': True} } LAND_VALIDATION_RULES: Final[Dict[str, Dict[str, Any]]] = { 'type': {'type': ('str', 'object'), 'required': True}, 'text': {'type': ('str', 'object'), 'required': False} } # Price checking configuration DEFAULT_PRICE_DELAY: Final[float] = 0.1 # Delay between price checks in seconds MAX_PRICE_CHECK_ATTEMPTS: Final[int] = 3 # Maximum attempts for price checking PRICE_CACHE_SIZE: Final[int] = 128 # Size of price check LRU cache PRICE_CHECK_TIMEOUT: Final[int] = 30 # Timeout for price check requests in seconds PRICE_TOLERANCE_MULTIPLIER: Final[float] = 1.1 # Multiplier for price tolerance DEFAULT_MAX_CARD_PRICE: Final[float] = 20.0 # Default maximum price per card # Deck composition defaults DEFAULT_RAMP_COUNT: Final[int] = 8 # Default number of ramp pieces DEFAULT_LAND_COUNT: Final[int] = 35 # Default total land count DEFAULT_BASIC_LAND_COUNT: Final[int] = 15 # Default minimum basic lands DEFAULT_NON_BASIC_LAND_SLOTS: Final[int] = 10 # Default number of non-basic land slots to reserve DEFAULT_BASICS_PER_COLOR: Final[int] = 5 # Default number of basic lands to add per color # Miscellaneous land configuration MISC_LAND_MIN_COUNT: Final[int] = 5 # Minimum number of miscellaneous lands to add MISC_LAND_MAX_COUNT: Final[int] = 10 # Maximum number of miscellaneous lands to add MISC_LAND_POOL_SIZE: Final[int] = 100 # Maximum size of initial land pool to select from MISC_LAND_TOP_POOL_SIZE: Final[int] = 30 # For utility step: sample from top N by EDHREC rank MISC_LAND_COLOR_FIX_PRIORITY_WEIGHT: Final[int] = 2 # Weight multiplier for color-fixing candidates MISC_LAND_USE_FULL_POOL: Final[bool] = True # If True, ignore TOP_POOL_SIZE and use entire remaining land pool for misc step MISC_LAND_EDHREC_KEEP_PERCENT: Final[float] = 0.80 # Legacy single-value fallback if min/max not set # When both min & max are defined (0 1, 'Exotic Orchard': lambda commander_tags, colors, commander_power: len(colors) > 1, 'War Room': lambda commander_tags, colors, commander_power: len(colors) <= 2, 'Rogue\'s Passage': lambda commander_tags, colors, commander_power: commander_power >= 5 } # Constants for land removal functionality LAND_REMOVAL_MAX_ATTEMPTS: Final[int] = 3 # Protected lands that cannot be removed during land removal process PROTECTED_LANDS: Final[List[str]] = BASIC_LANDS + KINDRED_LAND_NAMES # Other defaults DEFAULT_CREATURE_COUNT: Final[int] = 25 # Default number of creatures DEFAULT_REMOVAL_COUNT: Final[int] = 10 # Default number of spot removal spells DEFAULT_WIPES_COUNT: Final[int] = 2 # Default number of board wipes DEFAULT_CARD_ADVANTAGE_COUNT: Final[int] = 10 # Default number of card advantage pieces DEFAULT_PROTECTION_COUNT: Final[int] = 8 # Default number of protective effects (hexproof, indestructible, protection, ward, etc.) # Deck composition prompts DECK_COMPOSITION_PROMPTS: Final[Dict[str, str]] = { 'ramp': 'Enter desired number of ramp pieces (default: 8):', 'lands': 'Enter desired number of total lands (default: 35):', 'basic_lands': 'Enter minimum number of basic lands (default: 15):', 'fetch_lands': 'Enter desired number of fetch lands (default: 3):', 'creatures': 'Enter desired number of creatures (default: 25):', 'removal': 'Enter desired number of spot removal spells (default: 10):', 'wipes': 'Enter desired number of board wipes (default: 2):', 'card_advantage': 'Enter desired number of card advantage pieces (default: 10):', 'protection': 'Enter desired number of protective effects (default: 8):', 'max_deck_price': 'Enter maximum total deck price in dollars (default: 400.0):', 'max_card_price': 'Enter maximum price per card in dollars (default: 20.0):' } DEFAULT_MAX_DECK_PRICE: Final[float] = 400.0 # Default maximum total deck price BATCH_PRICE_CHECK_SIZE: Final[int] = 50 # Number of cards to check prices for in one batch # Constants for input validation # Type aliases CardName = str CardType = str ThemeTag = str ColorIdentity = str ColorList = List[str] ColorInfo = Tuple[str, List[str], List[str]] INPUT_VALIDATION = { 'max_attempts': 3, 'default_text_message': 'Please enter a valid text response.', 'default_number_message': 'Please enter a valid number.', 'default_confirm_message': 'Please enter Y/N or Yes/No.', 'default_choice_message': 'Please select a valid option from the list.' } QUESTION_TYPES = [ 'Text', 'Number', 'Confirm', 'Choice' ] # Constants for theme weight management and selection # Multiplier for initial card pool size during theme-based selection THEME_POOL_SIZE_MULTIPLIER: Final[float] = 2.0 # Bonus multiplier for cards that match multiple deck themes THEME_PRIORITY_BONUS: Final[float] = 1.2 # Safety multiplier to avoid overshooting target counts THEME_WEIGHT_MULTIPLIER: Final[float] = 0.9 THEME_WEIGHTS_DEFAULT: Final[Dict[str, float]] = { 'primary': 1.0, 'secondary': 0.6, 'tertiary': 0.2, 'hidden': 0.0 } WEIGHT_ADJUSTMENT_FACTORS: Final[Dict[str, float]] = { 'kindred_primary': 1.4, # Boost for Kindred themes as primary 'kindred_secondary': 1.3, # Boost for Kindred themes as secondary 'kindred_tertiary': 1.2, # Boost for Kindred themes as tertiary 'theme_synergy': 1.2 # Boost for themes that work well together } DEFAULT_THEME_TAGS = [ 'Aggro', 'Aristocrats', 'Artifacts Matter', 'Big Mana', 'Blink', 'Board Wipes', 'Burn', 'Cantrips', 'Card Draw', 'Clones', 'Combat Matters', 'Control', 'Counters Matter', 'Energy', 'Enter the Battlefield', 'Equipment', 'Exile Matters', 'Infect', 'Interaction', 'Lands Matter', 'Leave the Battlefield', 'Legends Matter', 'Life Matters', 'Mill', 'Monarch', 'Protective Effects', 'Ramp', 'Reanimate', 'Removal', 'Sacrifice Matters', 'Spellslinger', 'Stax', 'Superfriends', 'Theft', 'Token Creation', 'Tokens Matter', 'Voltron', 'X Spells' ] # CSV processing configuration CSV_READ_TIMEOUT: Final[int] = 30 # Timeout in seconds for CSV read operations CSV_PROCESSING_BATCH_SIZE: Final[int] = 1000 # Number of rows to process in each batch # CSV validation configuration CSV_VALIDATION_RULES: Final[Dict[str, Dict[str, Any]]] = { 'name': {'type': ('str', 'object'), 'required': True, 'unique': True}, 'edhrecRank': {'type': ('str', 'int', 'float', 'object'), 'min': 0, 'max': 100000}, 'manaValue': {'type': ('str', 'int', 'float', 'object'), 'min': 0, 'max': 20}, 'power': {'type': ('str', 'int', 'float', 'object'), 'pattern': r'^[\d*+-]+$'}, 'toughness': {'type': ('str', 'int', 'float', 'object'), 'pattern': r'^[\d*+-]+$'} } # (CSV_REQUIRED_COLUMNS imported from settings to avoid duplication) # DataFrame processing configuration BATCH_SIZE: Final[int] = 1000 # Number of records to process at once DATAFRAME_BATCH_SIZE: Final[int] = 500 # Batch size for DataFrame operations TRANSFORM_BATCH_SIZE: Final[int] = 250 # Batch size for data transformations CSV_DOWNLOAD_TIMEOUT: Final[int] = 30 # Timeout in seconds for CSV downloads PROGRESS_UPDATE_INTERVAL: Final[int] = 100 # Number of records between progress updates # DataFrame operation timeouts DATAFRAME_READ_TIMEOUT: Final[int] = 30 # Timeout for DataFrame read operations DATAFRAME_WRITE_TIMEOUT: Final[int] = 30 # Timeout for DataFrame write operations DATAFRAME_TRANSFORM_TIMEOUT: Final[int] = 45 # Timeout for DataFrame transformations DATAFRAME_VALIDATION_TIMEOUT: Final[int] = 20 # Timeout for DataFrame validation # Required DataFrame columns DATAFRAME_REQUIRED_COLUMNS: Final[List[str]] = [ 'name', 'type', 'colorIdentity', 'manaValue', 'text', 'edhrecRank', 'themeTags', 'keywords' ] # DataFrame validation rules DATAFRAME_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float, bool]]]] = { 'name': {'type': ('str', 'object'), 'required': True, 'unique': True}, 'edhrecRank': {'type': ('str', 'int', 'float', 'object'), 'min': 0, 'max': 100000}, 'manaValue': {'type': ('str', 'int', 'float', 'object'), 'min': 0, 'max': 20}, 'power': {'type': ('str', 'int', 'float', 'object'), 'pattern': r'^[\d*+-]+$'}, 'toughness': {'type': ('str', 'int', 'float', 'object'), 'pattern': r'^[\d*+-]+$'}, 'colorIdentity': {'type': ('str', 'object'), 'required': True}, 'text': {'type': ('str', 'object'), 'required': False} } # Card type sorting order for organizing libraries # This constant defines the order in which different card types should be sorted # when organizing a deck library. The order is designed to group cards logically, # starting with Planeswalkers and ending with Lands. CARD_TYPE_SORT_ORDER: Final[List[str]] = [ 'Planeswalker', 'Battle', 'Creature', 'Instant', 'Sorcery', 'Artifact', 'Enchantment', 'Land' ] # Game changer cards, used to help determine bracket GAME_CHANGERS: Final[List[str]] = [ 'Ad Nauseum', 'Ancient Tomb', 'Aura Shards', 'Bolas\'s Citadel', 'Braids, Cabal Minion', 'Chrome Mox', 'Coalition Victory', 'Consecrated Sphinx', 'Crop Rotation', 'Cyclonic Rift', 'Deflecting Swat', 'Demonic Tutor', 'Drannith Magistrate', 'Enlightened Tutor', 'Expropriate', 'Field of the Dead', 'Fierce Guardianship', 'Food Chain', 'Force of Will', 'Gaea\'s Cradle', 'Gamble', 'Gifts Ungiven', 'Glacial Chasm', 'Grand Arbiter Augustin IV', 'Grim Monolith', 'Humility', 'Imperial Seal', 'Intuition', 'Jeska\'s Will', 'Jin-Gitaxias, Core Augur', 'Kinnan, Bonder Prodigy', 'Lion\'s Eye Diamond', 'Mana Vault', 'Mishra\'s Workshop', 'Mox Diamond', 'Mystical Tutor', 'Narset, Parter of Veils', 'Natural Order', 'Necropotence', 'Notion Thief', 'Opposition Agent', 'Orcish Bowmasters', 'Panoptic Mirror', 'Rhystic Study', 'Seedborn Muse', 'Serra\'s Sanctum', 'Smother Tithe', 'Survival of the Fittest', 'Sway of the Stars', 'Teferi\'s Protection', 'Tergrid, God of Fright', 'Thassa\'s Oracle', 'The One Ring', 'The Tabernacle at Pendrell Vale', 'Underworld Breach', 'Urza, Lord High Artificer', 'Vampiric Tutor', 'Vorinclex, Voice of Hunger', 'Winota, Joiner of Forces', 'Worldly Tutor', 'Yuriko, the Tiger\'s Shadow' ] # --------------------------------------------------------------------------- # Multi-copy archetype configuration (centralized source of truth) # --------------------------------------------------------------------------- # Each entry describes a supported multi-copy archetype eligible for the choose-one flow. # Fields: # - id: machine id # - name: card name # - color_identity: list[str] of required color letters (subset must be in commander CI) # - printed_cap: int | None (None means no printed cap) # - exclusive_group: str | None (at most one from the same group) # - triggers: { tagsAny: list[str], tags_all: list[str] } # - default_count: int (default 25) # - rec_window: tuple[int,int] (recommendation window) # - thrumming_stone_synergy: bool # - type_hint: 'creature' | 'noncreature' MULTI_COPY_ARCHETYPES: Final[dict[str, dict[str, Any]]] = { 'cid_timeless_artificer': { 'id': 'cid_timeless_artificer', 'name': 'Cid, Timeless Artificer', 'color_identity': ['U','W'], 'printed_cap': None, 'exclusive_group': None, 'triggers': { 'tagsAny': ['artificer kindred', 'hero kindred', 'artifacts matter'], 'tags_all': [] }, 'default_count': 25, 'rec_window': (20,30), 'thrumming_stone_synergy': True, 'type_hint': 'creature' }, 'dragons_approach': { 'id': 'dragons_approach', 'name': "Dragon's Approach", 'color_identity': ['R'], 'printed_cap': None, 'exclusive_group': None, 'triggers': { 'tagsAny': ['burn','spellslinger','prowess','storm','copy','cascade','impulse draw','treasure','ramp','graveyard','mill','discard','recursion'], 'tags_all': [] }, 'default_count': 25, 'rec_window': (20,30), 'thrumming_stone_synergy': True, 'type_hint': 'noncreature' }, 'hare_apparent': { 'id': 'hare_apparent', 'name': 'Hare Apparent', 'color_identity': ['W'], 'printed_cap': None, 'exclusive_group': None, 'triggers': { 'tagsAny': ['rabbit kindred','tokens matter','aggro'], 'tags_all': [] }, 'default_count': 25, 'rec_window': (20,30), 'thrumming_stone_synergy': True, 'type_hint': 'creature' }, 'slime_against_humanity': { 'id': 'slime_against_humanity', 'name': 'Slime Against Humanity', 'color_identity': ['G'], 'printed_cap': None, 'exclusive_group': None, 'triggers': { 'tagsAny': ['tokens','tokens matter','go-wide','exile matters','ooze kindred','spells matter','spellslinger','graveyard','mill','discard','recursion','domain','self-mill','delirium','descend'], 'tags_all': [] }, 'default_count': 25, 'rec_window': (20,30), 'thrumming_stone_synergy': True, 'type_hint': 'noncreature' }, 'relentless_rats': { 'id': 'relentless_rats', 'name': 'Relentless Rats', 'color_identity': ['B'], 'printed_cap': None, 'exclusive_group': 'rats', 'triggers': { 'tagsAny': ['rats','swarm','aristocrats','sacrifice','devotion-b','lifedrain','graveyard','recursion'], 'tags_all': [] }, 'default_count': 25, 'rec_window': (20,30), 'thrumming_stone_synergy': True, 'type_hint': 'creature' }, 'rat_colony': { 'id': 'rat_colony', 'name': 'Rat Colony', 'color_identity': ['B'], 'printed_cap': None, 'exclusive_group': 'rats', 'triggers': { 'tagsAny': ['rats','swarm','aristocrats','sacrifice','devotion-b','lifedrain','graveyard','recursion'], 'tags_all': [] }, 'default_count': 25, 'rec_window': (20,30), 'thrumming_stone_synergy': True, 'type_hint': 'creature' }, 'seven_dwarves': { 'id': 'seven_dwarves', 'name': 'Seven Dwarves', 'color_identity': ['R'], 'printed_cap': 7, 'exclusive_group': None, 'triggers': { 'tagsAny': ['dwarf kindred','treasure','equipment','tokens','go-wide','tribal'], 'tags_all': [] }, 'default_count': 7, 'rec_window': (7,7), 'thrumming_stone_synergy': True, 'type_hint': 'creature' }, 'persistent_petitioners': { 'id': 'persistent_petitioners', 'name': 'Persistent Petitioners', 'color_identity': ['U'], 'printed_cap': None, 'exclusive_group': None, 'triggers': { 'tagsAny': ['mill','advisor kindred','control','defenders','walls','draw-go'], 'tags_all': [] }, 'default_count': 25, 'rec_window': (20,30), 'thrumming_stone_synergy': True, 'type_hint': 'creature' }, 'shadowborn_apostle': { 'id': 'shadowborn_apostle', 'name': 'Shadowborn Apostle', 'color_identity': ['B'], 'printed_cap': None, 'exclusive_group': None, 'triggers': { 'tagsAny': ['demon kindred','aristocrats','sacrifice','recursion','lifedrain'], 'tags_all': [] }, 'default_count': 25, 'rec_window': (20,30), 'thrumming_stone_synergy': True, 'type_hint': 'creature' }, 'nazgul': { 'id': 'nazgul', 'name': 'Nazgûl', 'color_identity': ['B'], 'printed_cap': 9, 'exclusive_group': None, 'triggers': { 'tagsAny': ['wraith kindred','ring','amass','orc','menace','aristocrats','sacrifice','devotion-b'], 'tags_all': [] }, 'default_count': 9, 'rec_window': (9,9), 'thrumming_stone_synergy': True, 'type_hint': 'creature' }, 'tempest_hawk': { 'id': 'tempest_hawk', 'name': 'Tempest Hawk', 'color_identity': ['W'], 'printed_cap': None, 'exclusive_group': None, 'triggers': { 'tagsAny': ['bird kindred','aggro'], 'tags_all': [] }, 'default_count': 25, 'rec_window': (20,30), 'thrumming_stone_synergy': True, 'type_hint': 'creature' }, 'templar_knight': { 'id': 'templar_knight', 'name': 'Templar Knight', 'color_identity': ['W'], 'printed_cap': None, 'exclusive_group': None, 'triggers': { 'tagsAny': ['aggro','human kindred','knight kindred','historic matters','artifacts matter'], 'tags_all': [] }, 'default_count': 25, 'rec_window': (20,30), 'thrumming_stone_synergy': True, 'type_hint': 'creature' }, } EXCLUSIVE_GROUPS: Final[dict[str, list[str]]] = { 'rats': ['relentless_rats', 'rat_colony'] } # Popular and iconic cards for fuzzy matching prioritization POPULAR_CARDS: Final[set[str]] = { # Most played removal spells 'Lightning Bolt', 'Swords to Plowshares', 'Path to Exile', 'Counterspell', 'Assassinate', 'Murder', 'Go for the Throat', 'Fatal Push', 'Doom Blade', 'Naturalize', 'Disenchant', 'Beast Within', 'Chaos Warp', 'Generous Gift', 'Anguished Unmaking', 'Vindicate', 'Putrefy', 'Terminate', 'Abrupt Decay', # Board wipes 'Wrath of God', 'Day of Judgment', 'Damnation', 'Pyroclasm', 'Anger of the Gods', 'Supreme Verdict', 'Austere Command', 'Cyclonic Rift', 'Toxic Deluge', 'Blasphemous Act', 'Starstorm', 'Earthquake', 'Hurricane', 'Pernicious Deed', # Card draw engines 'Rhystic Study', 'Mystic Remora', 'Phyrexian Arena', 'Necropotence', 'Sylvan Library', 'Consecrated Sphinx', 'Mulldrifter', 'Divination', 'Sign in Blood', 'Night\'s Whisper', 'Harmonize', 'Concentrate', 'Mind Spring', 'Stroke of Genius', 'Blue Sun\'s Zenith', 'Pull from Tomorrow', # Ramp spells 'Sol Ring', 'Rampant Growth', 'Cultivate', 'Kodama\'s Reach', 'Farseek', 'Nature\'s Lore', 'Three Visits', 'Sakura-Tribe Elder', 'Wood Elves', 'Farhaven Elf', 'Solemn Simulacrum', 'Commander\'s Sphere', 'Arcane Signet', 'Talisman of Progress', 'Talisman of Dominance', 'Talisman of Indulgence', 'Talisman of Impulse', 'Talisman of Unity', 'Fellwar Stone', 'Mind Stone', 'Thought Vessel', 'Worn Powerstone', 'Thran Dynamo', 'Gilded Lotus', # Tutors 'Demonic Tutor', 'Vampiric Tutor', 'Mystical Tutor', 'Enlightened Tutor', 'Worldly Tutor', 'Survival of the Fittest', 'Green Sun\'s Zenith', 'Chord of Calling', 'Natural Order', 'Idyllic Tutor', 'Steelshaper\'s Gift', # Protection 'Counterspell', 'Negate', 'Swan Song', 'Dispel', 'Force of Will', 'Force of Negation', 'Fierce Guardianship', 'Deflecting Swat', 'Teferi\'s Protection', 'Heroic Intervention', 'Boros Charm', 'Simic Charm', # Value creatures 'Eternal Witness', 'Snapcaster Mage', 'Mulldrifter', 'Acidic Slime', 'Reclamation Sage', 'Wood Elves', 'Farhaven Elf', 'Solemn Simulacrum', 'Oracle of Mul Daya', 'Azusa, Lost but Seeking', 'Ramunap Excavator', 'Courser of Kruphix', 'Titania, Protector of Argoth', 'Avenger of Zendikar', # Planeswalkers 'Jace, the Mind Sculptor', 'Liliana of the Veil', 'Elspeth, Sun\'s Champion', 'Chandra, Torch of Defiance', 'Garruk Wildspeaker', 'Ajani, Mentor of Heroes', 'Teferi, Hero of Dominaria', 'Vraska, Golgari Queen', 'Domri, Anarch of Bolas', # Combo pieces 'Thassa\'s Oracle', 'Laboratory Maniac', 'Jace, Wielder of Mysteries', 'Demonic Consultation', 'Tainted Pact', 'Ad Nauseam', 'Angel\'s Grace', 'Underworld Breach', 'Brain Freeze', 'Gaea\'s Cradle', 'Cradle of Vitality', # Equipment 'Lightning Greaves', 'Swiftfoot Boots', 'Sword of Fire and Ice', 'Sword of Light and Shadow', 'Sword of Feast and Famine', 'Umezawa\'s Jitte', 'Skullclamp', 'Cranial Plating', 'Bonesplitter', 'Loxodon Warhammer', # Enchantments 'Rhystic Study', 'Smothering Tithe', 'Phyrexian Arena', 'Sylvan Library', 'Mystic Remora', 'Necropotence', 'Doubling Season', 'Parallel Lives', 'Cathars\' Crusade', 'Impact Tremors', 'Purphoros, God of the Forge', # Artifacts (Commander-legal only) 'Sol Ring', 'Mana Vault', 'Chrome Mox', 'Mox Diamond', 'Lotus Petal', 'Lion\'s Eye Diamond', 'Sensei\'s Divining Top', 'Scroll Rack', 'Aetherflux Reservoir', 'Bolas\'s Citadel', 'The One Ring', # Lands 'Command Tower', 'Exotic Orchard', 'Reflecting Pool', 'City of Brass', 'Mana Confluence', 'Forbidden Orchard', 'Ancient Tomb', 'Reliquary Tower', 'Bojuka Bog', 'Strip Mine', 'Wasteland', 'Ghost Quarter', 'Tectonic Edge', 'Maze of Ith', 'Kor Haven', 'Riptide Laboratory', 'Academy Ruins', # Multicolored staples 'Lightning Helix', 'Electrolyze', 'Fire // Ice', 'Terminate', 'Putrefy', 'Vindicate', 'Anguished Unmaking', 'Abrupt Decay', 'Maelstrom Pulse', 'Sphinx\'s Revelation', 'Cruel Ultimatum', 'Nicol Bolas, Planeswalker', # Token generators 'Avenger of Zendikar', 'Hornet Queen', 'Tendershoot Dryad', 'Elspeth, Sun\'s Champion', 'Secure the Wastes', 'White Sun\'s Zenith', 'Decree of Justice', 'Empty the Warrens', 'Goblin Rabblemaster', 'Siege-Gang Commander', 'Krenko, Mob Boss', } ICONIC_CARDS: Final[set[str]] = { # Classic and iconic Magic cards that define the game (Commander-legal only) # Foundational spells 'Lightning Bolt', 'Counterspell', 'Swords to Plowshares', 'Dark Ritual', 'Giant Growth', 'Wrath of God', 'Fireball', 'Control Magic', 'Terror', 'Disenchant', 'Regrowth', 'Brainstorm', 'Force of Will', 'Wasteland', # Iconic creatures 'Tarmogoyf', 'Delver of Secrets', 'Snapcaster Mage', 'Dark Confidant', 'Psychatog', 'Morphling', 'Shivan Dragon', 'Serra Angel', 'Llanowar Elves', 'Birds of Paradise', 'Noble Hierarch', 'Deathrite Shaman', 'True-Name Nemesis', # Game-changing planeswalkers 'Jace, the Mind Sculptor', 'Liliana of the Veil', 'Elspeth, Knight-Errant', 'Chandra, Pyromaster', 'Garruk Wildspeaker', 'Ajani Goldmane', 'Nicol Bolas, Planeswalker', 'Karn Liberated', 'Ugin, the Spirit Dragon', # Combo enablers and engines 'Necropotence', 'Yawgmoth\'s Will', 'Show and Tell', 'Natural Order', 'Survival of the Fittest', 'Earthcraft', 'Squirrel Nest', 'High Tide', 'Reset', 'Time Spiral', 'Wheel of Fortune', 'Memory Jar', 'Windfall', # Iconic artifacts 'Sol Ring', 'Mana Vault', 'Winter Orb', 'Static Orb', 'Sphere of Resistance', 'Trinisphere', 'Chalice of the Void', 'Null Rod', 'Stony Silence', 'Crucible of Worlds', 'Sensei\'s Divining Top', 'Scroll Rack', 'Skullclamp', # Powerful lands 'Strip Mine', 'Mishra\'s Factory', 'Maze of Ith', 'Gaea\'s Cradle', 'Serra\'s Sanctum', 'Cabal Coffers', 'Urborg, Tomb of Yawgmoth', 'Fetchlands', 'Dual Lands', 'Shock Lands', 'Check Lands', # Magic history and format-defining cards 'Mana Drain', 'Daze', 'Ponder', 'Preordain', 'Path to Exile', 'Dig Through Time', 'Treasure Cruise', 'Gitaxian Probe', 'Cabal Therapy', 'Thoughtseize', 'Hymn to Tourach', 'Chain Lightning', 'Price of Progress', 'Stoneforge Mystic', 'Bloodbraid Elf', 'Vendilion Clique', 'Cryptic Command', # Commander format staples 'Command Tower', 'Rhystic Study', 'Cyclonic Rift', 'Demonic Tutor', 'Vampiric Tutor', 'Mystical Tutor', 'Enlightened Tutor', 'Worldly Tutor', 'Eternal Witness', 'Solemn Simulacrum', 'Consecrated Sphinx', 'Avenger of Zendikar', } # M4: Parquet filtering helpers def get_commanders(df: pd.DataFrame) -> pd.DataFrame: """Filter DataFrame to only commander-legal cards using isCommander flag. M4: Replaces CSV-based commander filtering with Parquet boolean flag. Args: df: DataFrame with 'isCommander' column Returns: Filtered DataFrame containing only commanders """ if 'isCommander' not in df.columns: return pd.DataFrame() return df[df['isCommander'] == True].copy() # noqa: E712 def get_backgrounds(df: pd.DataFrame) -> pd.DataFrame: """Filter DataFrame to only background cards using isBackground flag. M4: Replaces CSV-based background filtering with Parquet boolean flag. Args: df: DataFrame with 'isBackground' column Returns: Filtered DataFrame containing only backgrounds """ if 'isBackground' not in df.columns: return pd.DataFrame() return df[df['isBackground'] == True].copy() # noqa: E712