diff --git a/code/deck_builder/__init__.py b/code/deck_builder/__init__.py new file mode 100644 index 0000000..3f168f8 --- /dev/null +++ b/code/deck_builder/__init__.py @@ -0,0 +1,7 @@ +from .builder import DeckBuilder +from .builder_utils import * +from .builder_constants import * + +__all__ = [ + 'DeckBuilder', +] \ No newline at end of file diff --git a/deck_builder.py b/code/deck_builder/builder.py similarity index 98% rename from deck_builder.py rename to code/deck_builder/builder.py index 10cd5c7..e0eb0d8 100644 --- a/deck_builder.py +++ b/code/deck_builder/builder.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging import math import numpy as np import os @@ -16,8 +15,9 @@ import pprint from fuzzywuzzy import process from tqdm import tqdm -from settings import ( - BASIC_LANDS, CARD_TYPES, CSV_DIRECTORY, multiple_copy_cards, DEFAULT_NON_BASIC_LAND_SLOTS, +from settings import CSV_DIRECTORY, MULTIPLE_COPY_CARDS +from .builder_constants import ( + BASIC_LANDS, CARD_TYPES, DEFAULT_NON_BASIC_LAND_SLOTS, COMMANDER_CSV_PATH, FUZZY_MATCH_THRESHOLD, MAX_FUZZY_CHOICES, FETCH_LAND_DEFAULT_COUNT, COMMANDER_POWER_DEFAULT, COMMANDER_TOUGHNESS_DEFAULT, COMMANDER_MANA_COST_DEFAULT, COMMANDER_MANA_VALUE_DEFAULT, COMMANDER_TYPE_DEFAULT, COMMANDER_TEXT_DEFAULT, @@ -29,8 +29,8 @@ from settings import ( MISC_LAND_POOL_SIZE, LAND_REMOVAL_MAX_ATTEMPTS, PROTECTED_LANDS, MANA_COLORS, MANA_PIP_PATTERNS, THEME_WEIGHT_MULTIPLIER ) -import builder_utils -import setup_utils +from . import builder_utils +from file_setup import setup_utils from input_handler import InputHandler from exceptions import ( BasicLandCountError, @@ -78,6 +78,14 @@ from type_definitions import ( PlaneswalkerDF, NonPlaneswalkerDF) +import logging_util + +# Create logger for this module +logger = logging_util.logging.getLogger(__name__) +logger.setLevel(logging_util.LOG_LEVEL) +logger.addHandler(logging_util.file_handler) +logger.addHandler(logging_util.stream_handler) + # Try to import scrython and price_checker try: import scrython @@ -87,38 +95,9 @@ except ImportError: scrython = None PriceChecker = None use_scrython = False - logging.warning("Scrython is not installed. Price checking features will be unavailable." + logger.warning("Scrython is not installed. Price checking features will be unavailable." ) -# Create logs directory if it doesn't exist -if not os.path.exists('logs'): - os.makedirs('logs') - -# Logging configuration -LOG_DIR = 'logs' -LOG_FILE = f'{LOG_DIR}/deck_builder.log' -LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' -LOG_LEVEL = logging.INFO - -# Create formatters and handlers -formatter = logging.Formatter(LOG_FORMAT) - -# File handler -file_handler = logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8') -file_handler.setFormatter(formatter) - -# Stream handler -stream_handler = logging.StreamHandler() -stream_handler.setFormatter(formatter) - -# Create logger for this module -logger = logging.getLogger(__name__) -logger.setLevel(LOG_LEVEL) - -# Add handlers to logger -logger.addHandler(file_handler) -logger.addHandler(stream_handler) - pd.set_option('display.max_columns', None) pd.set_option('display.max_rows', None) pd.set_option('display.max_colwidth', 50) @@ -640,7 +619,7 @@ class DeckBuilder: Returns: True if color identity was handled, False otherwise """ - from settings import OTHER_COLOR_MAP + from builder_constants import OTHER_COLOR_MAP if color_identity in OTHER_COLOR_MAP: identity_info = OTHER_COLOR_MAP[color_identity] @@ -1137,7 +1116,7 @@ class DeckBuilder: PriceTimeoutError: If the price check times out PriceValidationError: If the price data is invalid """ - multiple_copies = BASIC_LANDS + multiple_copy_cards + multiple_copies = BASIC_LANDS + MULTIPLE_COPY_CARDS # Skip if card already exists and isn't allowed multiple copies if card in pd.Series(self.card_library['Card Name']).values and card not in multiple_copies: @@ -1303,7 +1282,7 @@ class DeckBuilder: """ try: # Get list of cards that can have duplicates - duplicate_lists = BASIC_LANDS + multiple_copy_cards + duplicate_lists = BASIC_LANDS + MULTIPLE_COPY_CARDS # Process duplicates using helper function self.card_library = builder_utils.process_duplicate_cards( @@ -2084,7 +2063,7 @@ class DeckBuilder: cards_added = [] for card in selected_cards: # Handle multiple copy cards - if card['name'] in multiple_copy_cards: + if card['name'] in MULTIPLE_COPY_CARDS: copies = { 'Nazgûl': 9, 'Seven Dwarves': 7 @@ -2196,7 +2175,7 @@ class DeckBuilder: continue # Handle multiple-copy cards - if card['name'] in multiple_copy_cards: + if card['name'] in MULTIPLE_COPY_CARDS: existing_copies = len(self.card_library[self.card_library['Card Name'] == card['name']]) if existing_copies < ideal_value: cards_to_add.append(card) diff --git a/code/deck_builder/builder_constants.py b/code/deck_builder/builder_constants.py new file mode 100644 index 0000000..e1aa217 --- /dev/null +++ b/code/deck_builder/builder_constants.py @@ -0,0 +1,437 @@ +from typing import Dict, List, Optional, Final, Tuple, Pattern, Union, Callable +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}' +COMMANDER_CSV_PATH: Final[str] = 'csv_files/commander_cards.csv' +DECK_DIRECTORY = '../deck_files' +COMMANDER_CONVERTERS: Final[Dict[str, str]] = {'themeTags': ast.literal_eval, 'creatureTypes': 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]] = [] + +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']) +} + +# 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] = 20 # 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 + +# Default fetch land count +FETCH_LAND_DEFAULT_COUNT: Final[int] = 3 # Default number of fetch lands to include + +# Basic Lands +BASIC_LANDS = ['Plains', 'Island', 'Swamp', 'Mountain', 'Forest'] + +# Basic land mappings +COLOR_TO_BASIC_LAND: Final[Dict[str, str]] = { + 'W': 'Plains', + 'U': 'Island', + 'B': 'Swamp', + 'R': 'Mountain', + 'G': 'Forest', + 'C': 'Wastes' +} + +# Dual land type mappings +DUAL_LAND_TYPE_MAP: Final[Dict[str, str]] = { + 'azorius': 'Plains Island', + 'dimir': 'Island Swamp', + 'rakdos': 'Swamp Mountain', + 'gruul': 'Mountain Forest', + 'selesnya': 'Forest Plains', + 'orzhov': 'Plains Swamp', + 'golgari': 'Swamp Forest', + 'simic': 'Forest Island', + 'izzet': 'Island Mountain', + 'boros': 'Mountain Plains' +} + +# Triple land type mappings +TRIPLE_LAND_TYPE_MAP: Final[Dict[str, str]] = { + 'bant': 'Forest Plains Island', + 'esper': 'Plains Island Swamp', + 'grixis': 'Island Swamp Mountain', + 'jund': 'Swamp Mountain Forest', + 'naya': 'Mountain Forest Plains', + 'mardu': 'Mountain Plains Swamp', + 'abzan': 'Plains Swamp Forest', + 'sultai': 'Swamp Forest Island', + 'temur': 'Forest Island Mountain', + 'jeskai': 'Island Mountain Plains' +} + +# Default preference for including dual lands +DEFAULT_DUAL_LAND_ENABLED: Final[bool] = True + +# Default preference for including triple lands +DEFAULT_TRIPLE_LAND_ENABLED: Final[bool] = True + +SNOW_COVERED_BASIC_LANDS: Final[Dict[str, str]] = { + 'W': 'Snow-Covered Plains', + 'U': 'Snow-Covered Island', + 'B': 'Snow-Covered Swamp', + 'G': 'Snow-Covered Forest' +} + +SNOW_BASIC_LAND_MAPPING: Final[Dict[str, str]] = { + 'W': 'Snow-Covered Plains', + 'U': 'Snow-Covered Island', + 'B': 'Snow-Covered Swamp', + 'R': 'Snow-Covered Mountain', + 'G': 'Snow-Covered Forest', + 'C': 'Wastes' # Note: No snow-covered version exists for Wastes +} + +# Generic fetch lands list +GENERIC_FETCH_LANDS: Final[List[str]] = [ + 'Evolving Wilds', + 'Terramorphic Expanse', + 'Shire Terrace', + 'Escape Tunnel', + 'Promising Vein', + 'Myriad Landscape', + 'Fabled Passage', + 'Terminal Moraine', + 'Prismatic Vista' +] + +# Kindred land constants +KINDRED_STAPLE_LANDS: Final[List[Dict[str, str]]] = [ + { + 'name': 'Path of Ancestry', + 'type': 'Land' + }, + { + 'name': 'Three Tree City', + 'type': 'Legendary Land' + }, + {'name': 'Cavern of Souls', 'type': 'Land'} +] + +# Color-specific fetch land mappings +COLOR_TO_FETCH_LANDS: Final[Dict[str, List[str]]] = { + 'W': [ + 'Flooded Strand', + 'Windswept Heath', + 'Marsh Flats', + 'Arid Mesa', + 'Brokers Hideout', + 'Obscura Storefront', + 'Cabaretti Courtyard' + ], + 'U': [ + 'Flooded Strand', + 'Polluted Delta', + 'Scalding Tarn', + 'Misty Rainforest', + 'Brokers Hideout', + 'Obscura Storefront', + 'Maestros Theater' + ], + 'B': [ + 'Polluted Delta', + 'Bloodstained Mire', + 'Marsh Flats', + 'Verdant Catacombs', + 'Obscura Storefront', + 'Maestros Theater', + 'Riveteers Overlook' + ], + 'R': [ + 'Bloodstained Mire', + 'Wooded Foothills', + 'Scalding Tarn', + 'Arid Mesa', + 'Maestros Theater', + 'Riveteers Overlook', + 'Cabaretti Courtyard' + ], + 'G': [ + 'Wooded Foothills', + 'Windswept Heath', + 'Verdant Catacombs', + 'Misty Rainforest', + 'Brokers Hideout', + 'Riveteers Overlook', + 'Cabaretti Courtyard' + ] +} + +# Staple land conditions mapping +STAPLE_LAND_CONDITIONS: Final[Dict[str, Callable[[List[str], List[str], int], bool]]] = { + 'Reliquary Tower': lambda commander_tags, colors, commander_power: True, # Always include + 'Ash Barrens': lambda commander_tags, colors, commander_power: 'Landfall' not in commander_tags, + 'Command Tower': lambda commander_tags, colors, commander_power: len(colors) > 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 + [land['name'] for land in KINDRED_STAPLE_LANDS] + +# 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 protection spells + +# 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: 20):', + '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 protection spells (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.3, + 'hidden': 0.0 +} + +WEIGHT_ADJUSTMENT_FACTORS: Final[Dict[str, float]] = { + 'kindred_primary': 1.5, # 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', 'Protection', 'Ramp', 'Reanimate', + 'Removal', 'Sacrifice Matters', 'Spellslinger', 'Stax', 'Super Friends', + '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, Union[str, int, float]]]] = { + '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*+-]+$'} +} + +# Required columns for CSV validation +CSV_REQUIRED_COLUMNS: Final[List[str]] = [ + 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', + 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', + 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' +] + +# 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' +] \ No newline at end of file diff --git a/builder_utils.py b/code/deck_builder/builder_utils.py similarity index 98% rename from builder_utils.py rename to code/deck_builder/builder_utils.py index 951c7fd..471a20c 100644 --- a/builder_utils.py +++ b/code/deck_builder/builder_utils.py @@ -28,8 +28,6 @@ Typical usage example: # Standard library imports import functools -import logging -import os import time from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union, cast @@ -56,7 +54,7 @@ from exceptions import ( ) from input_handler import InputHandler from price_check import PriceChecker -from settings import ( +from .builder_constants import ( CARD_TYPE_SORT_ORDER, COLOR_TO_BASIC_LAND, COMMANDER_CONVERTERS, COMMANDER_CSV_PATH, DATAFRAME_BATCH_SIZE, DATAFRAME_REQUIRED_COLUMNS, DATAFRAME_TRANSFORM_TIMEOUT, @@ -72,35 +70,13 @@ from settings import ( WEIGHT_ADJUSTMENT_FACTORS ) from type_definitions import CardLibraryDF, CommanderDF, LandDF - -# Create logs directory if it doesn't exist -if not os.path.exists('logs'): - os.makedirs('logs') - -# Logging configuration -LOG_DIR = 'logs' -LOG_FILE = f'{LOG_DIR}/builder_utils.log' -LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' -LOG_LEVEL = logging.INFO - -# Create formatters and handlers -formatter = logging.Formatter(LOG_FORMAT) - -# File handler -file_handler = logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8') -file_handler.setFormatter(formatter) - -# Stream handler -stream_handler = logging.StreamHandler() -stream_handler.setFormatter(formatter) +import logging_util # Create logger for this module -logger = logging.getLogger(__name__) -logger.setLevel(LOG_LEVEL) - -# Add handlers to logger -logger.addHandler(file_handler) -logger.addHandler(stream_handler) +logger = logging_util.logging.getLogger(__name__) +logger.setLevel(logging_util.LOG_LEVEL) +logger.addHandler(logging_util.file_handler) +logger.addHandler(logging_util.stream_handler) # Type variables for generic functions T = TypeVar('T') @@ -993,7 +969,7 @@ def get_available_kindred_lands(land_df: pd.DataFrame, colors: List[str], comman # Find lands specific to each creature type for creature_type in creature_types: - logging.info(f'Searching for {creature_type}-specific lands') + logger.info(f'Searching for {creature_type}-specific lands') # Filter lands by creature type mentions in text or type type_specific = land_df[ diff --git a/exceptions.py b/code/exceptions.py similarity index 100% rename from exceptions.py rename to code/exceptions.py diff --git a/code/file_setup/__init__.py b/code/file_setup/__init__.py new file mode 100644 index 0000000..a624832 --- /dev/null +++ b/code/file_setup/__init__.py @@ -0,0 +1,8 @@ +"""Initialize the file_setup package.""" + +from .setup import setup, regenerate_csv_by_color + +__all__ = [ + 'setup', + 'regenerate_csv_by_color' +] \ No newline at end of file diff --git a/setup.py b/code/file_setup/setup.py similarity index 92% rename from setup.py rename to code/file_setup/setup.py index 51b0fb4..a9612be 100644 --- a/setup.py +++ b/code/file_setup/setup.py @@ -28,15 +28,11 @@ from typing import Union, List, Dict, Any import inquirer import pandas as pd -# Local application imports -from settings import ( - banned_cards, - COLOR_ABRV, - CSV_DIRECTORY, - MTGJSON_API_URL, - SETUP_COLORS -) -from setup_utils import ( +# Local imports +import logging_util +from settings import CSV_DIRECTORY +from .setup_constants import BANNED_CARDS, SETUP_COLORS, COLOR_ABRV, MTGJSON_API_URL +from .setup_utils import ( download_cards_csv, filter_by_color_identity, filter_dataframe, @@ -50,34 +46,15 @@ from exceptions import ( MTGJSONDownloadError ) -# Create logs directory if it doesn't exist -if not os.path.exists('logs'): - os.makedirs('logs') - -# Logging configuration -LOG_DIR = 'logs' -LOG_FILE = f'{LOG_DIR}/setup.log' -LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' -LOG_LEVEL = logging.INFO - -# Create formatters and handlers -formatter = logging.Formatter(LOG_FORMAT) - -# File handler -file_handler = logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8') -file_handler.setFormatter(formatter) - -# Stream handler -stream_handler = logging.StreamHandler() -stream_handler.setFormatter(formatter) - # Create logger for this module -logger = logging.getLogger(__name__) -logger.setLevel(LOG_LEVEL) +logger = logging_util.logging.getLogger(__name__) +logger.setLevel(logging_util.LOG_LEVEL) +logger.addHandler(logging_util.file_handler) +logger.addHandler(logging_util.stream_handler) -# Add handlers to logger -logger.addHandler(file_handler) -logger.addHandler(stream_handler) +# Create CSV directory if it doesn't exist +if not os.path.exists(CSV_DIRECTORY): + os.makedirs(CSV_DIRECTORY) def check_csv_exists(file_path: Union[str, Path]) -> bool: """Check if a CSV file exists at the specified path. @@ -208,7 +185,7 @@ def determine_commanders() -> None: # Apply standard filters logger.info('Applying standard card filters') - filtered_df = filter_dataframe(filtered_df, banned_cards) + filtered_df = filter_dataframe(filtered_df, BANNED_CARDS) # Save commander cards logger.info('Saving validated commander cards') diff --git a/code/file_setup/setup_constants.py b/code/file_setup/setup_constants.py new file mode 100644 index 0000000..31582bc --- /dev/null +++ b/code/file_setup/setup_constants.py @@ -0,0 +1,118 @@ +from typing import Dict, List, Optional, Final, Tuple, Pattern, Union, Callable + +BANNED_CARDS: List[str] = [# in commander + 'Ancestral Recall', 'Balance', 'Biorhythm', 'Black Lotus', + 'Braids, Cabal Minion', 'Chaos Orb', 'Coalition Victory', + 'Channel', 'Dockside Extortionist', 'Emrakul, the Aeons Torn', + 'Erayo, Soratami Ascendant', 'Falling Star', 'Fastbond', + 'Flash', 'Gifts Ungiven', 'Golos, Tireless Pilgrim', + 'Griselbrand', 'Hullbreacher', 'Iona, Shield of Emeria', + 'Karakas', 'Jeweled Lotus', 'Leovold, Emissary of Trest', + 'Library of Alexandria', 'Limited Resources', 'Lutri, the Spellchaser', + 'Mana Crypt', 'Mox Emerald', 'Mox Jet', 'Mox Pearl', 'Mox Ruby', + 'Mox Sapphire', 'Nadu, Winged Wisdom', 'Panoptic Mirror', + 'Paradox Engine', 'Primeval Titan', 'Prophet of Kruphix', + 'Recurring Nightmare', 'Rofellos, Llanowar Emissary', 'Shahrazad', + 'Sundering Titan', 'Sway of the Stars', 'Sylvan Primordial', + 'Time Vault', 'Time Walk', 'Tinker', 'Tolarian Academy', + 'Trade Secrets', 'Upheaval', 'Yawgmoth\'s Bargain', + + # In constructed + 'Invoke Prejudice', 'Cleanse', 'Stone-Throwing Devils', 'Pradesh Gypsies', + 'Jihad', 'Imprison', 'Crusade' + ] + +SETUP_COLORS: List[str] = ['colorless', 'white', 'blue', 'black', 'green', 'red', + 'azorius', 'orzhov', 'selesnya', 'boros', 'dimir', + 'simic', 'izzet', 'golgari', 'rakdos', 'gruul', + 'bant', 'esper', 'grixis', 'jund', 'naya', + 'abzan', 'jeskai', 'mardu', 'sultai', 'temur', + 'dune', 'glint', 'ink', 'witch', 'yore', 'wubrg'] + +COLOR_ABRV: List[str] = ['Colorless', 'W', 'U', 'B', 'G', 'R', + 'U, W', 'B, W', 'G, W', 'R, W', 'B, U', + 'G, U', 'R, U', 'B, G', 'B, R', 'G, R', + 'G, U, W', 'B, U, W', 'B, R, U', 'B, G, R', 'G, R, W', + 'B, G, W', 'R, U, W', 'B, R, W', 'B, G, U', 'G, R, U', + 'B, G, R, W', 'B, G, R, U', 'G, R, U, W', 'B, G, U, W', + 'B, R, U, W', 'B, G, R, U, W'] + +# Constants for setup and CSV processing +MTGJSON_API_URL: str = 'https://mtgjson.com/api/v5/csv/cards.csv' + +LEGENDARY_OPTIONS: List[str] = [ + 'Legendary Creature', + 'Legendary Artifact', + 'Legendary Artifact Creature', + 'Legendary Enchantment Creature', + 'Legendary Planeswalker' +] + +NON_LEGAL_SETS: List[str] = [ + 'PHTR', 'PH17', 'PH18', 'PH19', 'PH20', 'PH21', + 'UGL', 'UND', 'UNH', 'UST' +] + +CARD_TYPES_TO_EXCLUDE: List[str] = [ + 'Plane —', + 'Conspiracy', + 'Vanguard', + 'Scheme', + 'Phenomenon', + 'Stickers', + 'Attraction', + 'Hero', + 'Contraption' +] + +# Columns to keep when processing CSV files +CSV_PROCESSING_COLUMNS: List[str] = [ + 'name', # Card name + 'faceName', # Name of specific face for multi-faced cards + 'edhrecRank', # Card's rank on EDHREC + 'colorIdentity', # Color identity for Commander format + 'colors', # Actual colors in card's mana cost + 'manaCost', # Mana cost string + 'manaValue', # Converted mana cost + 'type', # Card type line + 'layout', # Card layout (normal, split, etc) + 'text', # Card text/rules + 'power', # Power (for creatures) + 'toughness', # Toughness (for creatures) + 'keywords', # Card's keywords + 'side' # Side identifier for multi-faced cards +] + +# Configuration for DataFrame sorting operations +SORT_CONFIG = { + 'columns': ['name', 'side'], # Columns to sort by + 'case_sensitive': False # Ignore case when sorting +} + +# Configuration for DataFrame filtering operations +FILTER_CONFIG: Dict[str, Dict[str, List[str]]] = { + 'layout': { + 'exclude': ['reversible_card'] + }, + 'availability': { + 'require': ['paper'] + }, + 'promoTypes': { + 'exclude': ['playtest'] + }, + 'securityStamp': { + 'exclude': ['Heart', 'Acorn'] + } +} + +COLUMN_ORDER: List[str] = [ + 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', + 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', + 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' +] + +TAGGED_COLUMN_ORDER: List[str] = [ + 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', + 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', + 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' +] \ No newline at end of file diff --git a/setup_utils.py b/code/file_setup/setup_utils.py similarity index 95% rename from setup_utils.py rename to code/file_setup/setup_utils.py index cf91b82..d10a19a 100644 --- a/setup_utils.py +++ b/code/file_setup/setup_utils.py @@ -28,17 +28,14 @@ import pandas as pd from tqdm import tqdm # Local application imports -from settings import ( +from .setup_constants import ( CSV_PROCESSING_COLUMNS, CARD_TYPES_TO_EXCLUDE, NON_LEGAL_SETS, LEGENDARY_OPTIONS, - FILL_NA_COLUMNS, SORT_CONFIG, FILTER_CONFIG, COLUMN_ORDER, - PRETAG_COLUMN_ORDER, - EXCLUDED_CARD_TYPES, TAGGED_COLUMN_ORDER ) from exceptions import ( @@ -48,35 +45,14 @@ from exceptions import ( CommanderValidationError ) from type_definitions import CardLibraryDF - -# Create logs directory if it doesn't exist -if not os.path.exists('logs'): - os.makedirs('logs') - -# Logging configuration -LOG_DIR = 'logs' -LOG_FILE = f'{LOG_DIR}/setup_utils.log' -LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' -LOG_LEVEL = logging.INFO - -# Create formatters and handlers -formatter = logging.Formatter(LOG_FORMAT) - -# File handler -file_handler = logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8') -file_handler.setFormatter(formatter) - -# Stream handler -stream_handler = logging.StreamHandler() -stream_handler.setFormatter(formatter) +from settings import FILL_NA_COLUMNS +import logging_util # Create logger for this module -logger = logging.getLogger(__name__) -logger.setLevel(LOG_LEVEL) - -# Add handlers to logger -logger.addHandler(file_handler) -logger.addHandler(stream_handler) +logger = logging_util.logging.getLogger(__name__) +logger.setLevel(logging_util.LOG_LEVEL) +logger.addHandler(logging_util.file_handler) +logger.addHandler(logging_util.stream_handler) # Type definitions class FilterRule(TypedDict): diff --git a/input_handler.py b/code/input_handler.py similarity index 95% rename from input_handler.py rename to code/input_handler.py index db473dc..811dfd8 100644 --- a/input_handler.py +++ b/code/input_handler.py @@ -8,7 +8,9 @@ from typing import Any, List, Optional, Tuple, Union import inquirer.prompt from settings import ( - COLORS, COLOR_ABRV, DEFAULT_MAX_CARD_PRICE, + COLORS, COLOR_ABRV +) +from deck_builder.builder_constants import (DEFAULT_MAX_CARD_PRICE, DEFAULT_MAX_DECK_PRICE, DEFAULT_THEME_TAGS, MONO_COLOR_MAP, DUAL_COLOR_MAP, TRI_COLOR_MAP, OTHER_COLOR_MAP ) @@ -28,36 +30,13 @@ from exceptions import ( PriceLimitError, PriceValidationError ) - -# Create logs directory if it doesn't exist -if not os.path.exists('logs'): - os.makedirs('logs') - -# Logging configuration -LOG_DIR = 'logs' -LOG_FILE = f'{LOG_DIR}/input_handler.log' -LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' -LOG_LEVEL = logging.INFO - -# Create formatters and handlers -formatter = logging.Formatter(LOG_FORMAT) - -# File handler -file_handler = logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8') -file_handler.setFormatter(formatter) - -# Stream handler -stream_handler = logging.StreamHandler() -stream_handler.setFormatter(formatter) +import logging_util # Create logger for this module -logger = logging.getLogger(__name__) -logger.setLevel(LOG_LEVEL) - -# Add handlers to logger -logger.addHandler(file_handler) -logger.addHandler(stream_handler) - +logger = logging_util.logging.getLogger(__name__) +logger.setLevel(logging_util.LOG_LEVEL) +logger.addHandler(logging_util.file_handler) +logger.addHandler(logging_util.stream_handler) class InputHandler: """Handles user input operations with validation and error handling. diff --git a/code/logging_util.py b/code/logging_util.py new file mode 100644 index 0000000..695fecb --- /dev/null +++ b/code/logging_util.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from settings import os +import logging + +# Create logs directory if it doesn't exist +if not os.path.exists('logs'): + os.makedirs('logs') + +# Logging configuration +LOG_DIR = 'logs' +LOG_FILE = os.path.join(LOG_DIR, 'deck_builder.log') +LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' +LOG_LEVEL = logging.INFO + +# Create formatters and handlers +# Create a formatter that removes double underscores +class NoDunderFormatter(logging.Formatter): + def format(self, record): + record.name = record.name.replace("__", "") + return super().format(record) + +# File handler +file_handler = logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8') +file_handler.setFormatter(NoDunderFormatter(LOG_FORMAT)) + +# Stream handler +stream_handler = logging.StreamHandler() +stream_handler.setFormatter(NoDunderFormatter(LOG_FORMAT)) \ No newline at end of file diff --git a/main.py b/code/main.py similarity index 78% rename from main.py rename to code/main.py index f33571c..e9f8ef5 100644 --- a/main.py +++ b/code/main.py @@ -9,8 +9,6 @@ from __future__ import annotations # Standard library imports import sys -import logging -import os from pathlib import Path from typing import NoReturn, Optional @@ -18,38 +16,16 @@ from typing import NoReturn, Optional import inquirer.prompt # Local imports -import deck_builder -import setup -import tagger - -# Create logs directory if it doesn't exist -if not os.path.exists('logs'): - os.makedirs('logs') - -# Logging configuration -LOG_DIR = 'logs' -LOG_FILE = os.path.join(LOG_DIR, 'main.log') -LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' -LOG_LEVEL = logging.INFO - -# Create formatters and handlers -formatter = logging.Formatter(LOG_FORMAT) - -# File handler -file_handler = logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8') -file_handler.setFormatter(formatter) - -# Stream handler -stream_handler = logging.StreamHandler() -stream_handler.setFormatter(formatter) +from deck_builder import DeckBuilder +from file_setup import setup +from tagging import tagger +import logging_util # Create logger for this module -logger = logging.getLogger(__name__) -logger.setLevel(LOG_LEVEL) - -# Add handlers to logger -logger.addHandler(file_handler) -logger.addHandler(stream_handler) +logger = logging_util.logging.getLogger(__name__) +logger.setLevel(logging_util.LOG_LEVEL) +logger.addHandler(logging_util.file_handler) +logger.addHandler(logging_util.stream_handler) # Menu constants MENU_SETUP = 'Setup' @@ -58,6 +34,8 @@ MENU_BUILD_DECK = 'Build a Deck' MENU_QUIT = 'Quit' MENU_CHOICES = [MENU_SETUP, MAIN_TAG, MENU_BUILD_DECK, MENU_QUIT] + +builder = DeckBuilder() def get_menu_choice() -> Optional[str]: """Display the main menu and get user choice. @@ -124,11 +102,11 @@ def run_menu() -> NoReturn: match choice: case 'Setup': - setup.setup() + setup() case 'Tag CSV Files': tagger.run_tagging() case 'Build a Deck': - deck_builder.main() + builder.determine_commander() case 'Quit': logger.info("Exiting application") sys.exit(0) diff --git a/price_check.py b/code/price_check.py similarity index 91% rename from price_check.py rename to code/price_check.py index 2c3cbfd..e4f1fbb 100644 --- a/price_check.py +++ b/code/price_check.py @@ -8,7 +8,6 @@ price lookups. from __future__ import annotations # Standard library imports -import logging import time from functools import lru_cache from typing import Dict, List, Optional, Tuple, Union @@ -24,7 +23,7 @@ from exceptions import ( PriceTimeoutError, PriceValidationError ) -from settings import ( +from deck_builder.builder_constants import ( BATCH_PRICE_CHECK_SIZE, DEFAULT_MAX_CARD_PRICE, DEFAULT_MAX_DECK_PRICE, @@ -35,31 +34,13 @@ from settings import ( PRICE_TOLERANCE_MULTIPLIER ) from type_definitions import PriceCache - -# Logging configuration -LOG_DIR = 'logs' -LOG_FILE = f'{LOG_DIR}/price_check.log' -LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' -LOG_LEVEL = logging.INFO - -# Create formatters and handlers -formatter = logging.Formatter(LOG_FORMAT) - -# File handler -file_handler = logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8') -file_handler.setFormatter(formatter) - -# Stream handler -stream_handler = logging.StreamHandler() -stream_handler.setFormatter(formatter) +import logging_util # Create logger for this module -logger = logging.getLogger(__name__) -logger.setLevel(LOG_LEVEL) - -# Add handlers to logger -logger.addHandler(file_handler) -logger.addHandler(stream_handler) +logger = logging_util.logging.getLogger(__name__) +logger.setLevel(logging_util.LOG_LEVEL) +logger.addHandler(logging_util.file_handler) +logger.addHandler(logging_util.stream_handler) class PriceChecker: """Class for handling MTG card price checking and validation. diff --git a/code/settings.py b/code/settings.py new file mode 100644 index 0000000..6b63e09 --- /dev/null +++ b/code/settings.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +# Standard library imports +import os +from sys import exit +from typing import Dict, List, Optional, Final, Tuple, Pattern, Union, Callable + +# Third-party imports + +COLORS = ['colorless', 'white', 'blue', 'black', 'red', 'green', + 'azorius', 'orzhov', 'selesnya', 'boros', 'dimir', + 'simic', 'izzet', 'golgari', 'rakdos', 'gruul', + 'bant', 'esper', 'grixis', 'jund', 'naya', + 'abzan', 'jeskai', 'mardu', 'sultai', 'temur', + 'dune', 'glint', 'ink', 'witch', 'yore', 'wubrg', + 'commander'] + +COLOR_ABRV: List[str] = ['Colorless', 'W', 'U', 'B', 'G', 'R', + 'U, W', 'B, W', 'G, W', 'R, W', 'B, U', + 'G, U', 'R, U', 'B, G', 'B, R', 'G, R', + 'G, U, W', 'B, U, W', 'B, R, U', 'B, G, R', 'G, R, W', + 'B, G, W', 'R, U, W', 'B, R, W', 'B, G, U', 'G, R, U', + 'B, G, R, W', 'B, G, R, U', 'G, R, U, W', 'B, G, U, W', + 'B, R, U, W', 'B, G, R, U, W'] + +MAIN_MENU_ITEMS: List[str] = ['Build A Deck', 'Setup CSV Files', 'Tag CSV Files', 'Quit'] + +SETUP_MENU_ITEMS: List[str] = ['Initial Setup', 'Regenerate CSV', 'Main Menu'] + +CSV_DIRECTORY: str = 'csv_files' + +# Configuration for handling null/NA values in DataFrame columns +FILL_NA_COLUMNS: Dict[str, Optional[str]] = { + 'colorIdentity': 'Colorless', # Default color identity for cards without one + 'faceName': None # Use card's name column value when face name is not available +} + +MULTIPLE_COPY_CARDS = ['Dragon\'s Approach', 'Hare Apparent', 'Nazgûl', 'Persistent Petitioners', + 'Rat Colony', 'Relentless Rats', 'Seven Dwarves', 'Shadowborn Apostle', + 'Slime Against Humanity', 'Templar Knight'] \ No newline at end of file diff --git a/code/tagging/__init__.py b/code/tagging/__init__.py new file mode 100644 index 0000000..64eaede --- /dev/null +++ b/code/tagging/__init__.py @@ -0,0 +1,4 @@ +""" +This module initializes the tagging package, which contains functionality for +handling card tagging and related operations in the MTG deck builder application. +""" \ No newline at end of file diff --git a/code/tagging/tag_constants.py b/code/tagging/tag_constants.py new file mode 100644 index 0000000..091dfcc --- /dev/null +++ b/code/tagging/tag_constants.py @@ -0,0 +1,718 @@ +from typing import Dict, List, Optional, Final, Tuple, Pattern, Union, Callable + +TRIGGERS: List[str] = ['when', 'whenever', 'at'] + +NUM_TO_SEARCH: List[str] = ['a', 'an', 'one', '1', 'two', '2', 'three', '3', 'four','4', 'five', '5', + 'six', '6', 'seven', '7', 'eight', '8', 'nine', '9', 'ten', '10', + 'x','one or more'] + + +# Constants for common tag groupings +TAG_GROUPS: Dict[str, List[str]] = { + "Cantrips": ["Cantrips", "Card Draw", "Spellslinger", "Spells Matter"], + "Tokens": ["Token Creation", "Tokens Matter"], + "Counters": ["Counters Matter"], + "Combat": ["Combat Matters", "Combat Tricks"], + "Artifacts": ["Artifacts Matter", "Artifact Tokens"], + "Enchantments": ["Enchantments Matter", "Enchantment Tokens"], + "Lands": ["Lands Matter"], + "Spells": ["Spellslinger", "Spells Matter"] +} + +# Common regex patterns +PATTERN_GROUPS: Dict[str, Optional[str]] = { + "draw": r"draw[s]? a card|draw[s]? one card", + "combat": r"attack[s]?|block[s]?|combat damage", + "tokens": r"create[s]? .* token|put[s]? .* token", + "counters": r"\+1/\+1 counter|\-1/\-1 counter|loyalty counter", + "sacrifice": r"sacrifice[s]? .*|sacrificed", + "exile": r"exile[s]? .*|exiled", + "cost_reduction": r"cost[s]? \{[\d\w]\} less|affinity for|cost[s]? less to cast|chosen type cost|copy cost|from exile cost|from exile this turn cost|from your graveyard cost|has undaunted|have affinity for artifacts|other than your hand cost|spells cost|spells you cast cost|that target .* cost|those spells cost|you cast cost|you pay cost" +} + +# Creature/Counter types +COUNTER_TYPES: List[str] = [r'\+0/\+1', r'\+0/\+2', r'\+1/\+0', r'\+1/\+2', r'\+2/\+0', r'\+2/\+2', + '-0/-1', '-0/-2', '-1/-0', '-1/-2', '-2/-0', '-2/-2', + 'Acorn', 'Aegis', 'Age', 'Aim', 'Arrow', 'Arrowhead','Awakening', + 'Bait', 'Blaze', 'Blessing', 'Blight',' Blood', 'Bloddline', + 'Bloodstain', 'Book', 'Bounty', 'Brain', 'Bribery', 'Brick', + 'Burden', 'Cage', 'Carrion', 'Charge', 'Coin', 'Collection', + 'Component', 'Contested', 'Corruption', 'CRANK!', 'Credit', + 'Croak', 'Corpse', 'Crystal', 'Cube', 'Currency', 'Death', + 'Defense', 'Delay', 'Depletion', 'Descent', 'Despair', 'Devotion', + 'Divinity', 'Doom', 'Dream', 'Duty', 'Echo', 'Egg', 'Elixir', + 'Ember', 'Energy', 'Enlightened', 'Eon', 'Eruption', 'Everything', + 'Experience', 'Eyeball', 'Eyestalk', 'Fade', 'Fate', 'Feather', + 'Feeding', 'Fellowship', 'Fetch', 'Filibuster', 'Finality', 'Flame', + 'Flood', 'Foreshadow', 'Fungus', 'Fury', 'Fuse', 'Gem', 'Ghostform', + 'Glpyh', 'Gold', 'Growth', 'Hack', 'Harmony', 'Hatching', 'Hatchling', + 'Healing', 'Hit', 'Hope',' Hone', 'Hoofprint', 'Hour', 'Hourglass', + 'Hunger', 'Ice', 'Imposter', 'Incarnation', 'Incubation', 'Infection', + 'Influence', 'Ingenuity', 'Intel', 'Intervention', 'Invitation', + 'Isolation', 'Javelin', 'Judgment', 'Keyword', 'Ki', 'Kick', + 'Knickknack', 'Knowledge', 'Landmark', 'Level', 'Loot', 'Lore', + 'Loyalty', 'Luck', 'Magnet', 'Manabond', 'Manifestation', 'Mannequin', + 'Mask', 'Matrix', 'Memory', 'Midway', 'Mine', 'Mining', 'Mire', + 'Music', 'Muster', 'Necrodermis', 'Nest', 'Net', 'Night', 'Oil', + 'Omen', 'Ore', 'Page', 'Pain', 'Palliation', 'Paralyzing', 'Pause', + 'Petal', 'Petrification', 'Phyresis', 'Phylatery', 'Pin', 'Plague', + 'Plot', 'Point', 'Poison', 'Polyp', 'Possession', 'Pressure', 'Prey', + 'Pupa', 'Quest', 'Rad', 'Rejection', 'Reprieve', 'Rev', 'Revival', + 'Ribbon', 'Ritual', 'Rope', 'Rust', 'Scream', 'Scroll', 'Shell', + 'Shield', 'Silver', 'Shred', 'Sleep', 'Sleight', 'Slime', 'Slumber', + 'Soot', 'Soul', 'Spark', 'Spite', 'Spore', 'Stash', 'Storage', + 'Story', 'Strife', 'Study', 'Stun', 'Supply', 'Suspect', 'Takeover', + 'Task', 'Ticket', 'Tide', 'Time', 'Tower', 'Training', 'Trap', + 'Treasure', 'Unity', 'Unlock', 'Valor', 'Velocity', 'Verse', + 'Vitality', 'Void', 'Volatile', 'Vortex', 'Vow', 'Voyage', 'Wage', + 'Winch', 'Wind', 'Wish'] + +CREATURE_TYPES: List[str] = ['Advisor', 'Aetherborn', 'Alien', 'Ally', 'Angel', 'Antelope', 'Ape', 'Archer', 'Archon', 'Armadillo', + 'Army', 'Artificer', 'Assassin', 'Assembly-Worker', 'Astartes', 'Atog', 'Aurochs', 'Automaton', + 'Avatar', 'Azra', 'Badger', 'Balloon', 'Barbarian', 'Bard', 'Basilisk', 'Bat', 'Bear', 'Beast', 'Beaver', + 'Beeble', 'Beholder', 'Berserker', 'Bird', 'Blinkmoth', 'Boar', 'Brainiac', 'Bringer', 'Brushwagg', + 'C\'tan', 'Camarid', 'Camel', 'Capybara', 'Caribou', 'Carrier', 'Cat', 'Centaur', 'Chicken', 'Child', + 'Chimera', 'Citizen', 'Cleric', 'Clown', 'Cockatrice', 'Construct', 'Coward', 'Coyote', 'Crab', 'Crocodile', + 'Custodes', 'Cyberman', 'Cyclops', 'Dalek', 'Dauthi', 'Demigod', 'Demon', 'Deserter', 'Detective', 'Devil', + 'Dinosaur', 'Djinn', 'Doctor', 'Dog', 'Dragon', 'Drake', 'Dreadnought', 'Drone', 'Druid', 'Dryad', 'Dwarf', + 'Efreet', 'Egg', 'Elder', 'Eldrazi', 'Elemental', 'Elephant', 'Elf', 'Elk', 'Employee', 'Eye', 'Faerie', + 'Ferret', 'Fish', 'Flagbearer', 'Fox', 'Fractal', 'Frog', 'Fungus', 'Gamer', 'Gargoyle', 'Germ', 'Giant', + 'Gith', 'Glimmer', 'Gnoll', 'Gnome', 'Goat', 'Goblin', 'God', 'Golem', 'Gorgon', 'Graveborn', 'Gremlin', + 'Griffin', 'Guest', 'Hag', 'Halfling', 'Hamster', 'Harpy', 'Head', 'Hellion', 'Hero', 'Hippo', 'Hippogriff', + 'Homarid', 'Homunculus', 'Hornet', 'Horror', 'Horse', 'Human', 'Hydra', 'Hyena', 'Illusion', 'Imp', + 'Incarnation', 'Inkling', 'Inquisitor', 'Insect', 'Jackal', 'Jellyfish', 'Juggernaut', 'Kavu', 'Kirin', + 'Kithkin', 'Knight', 'Kobold', 'Kor', 'Kraken', 'Lamia', 'Lammasu', 'Leech', 'Leviathan', 'Lhurgoyf', + 'Licid', 'Lizard', 'Manticore', 'Masticore', 'Mercenary', 'Merfolk', 'Metathran', 'Minion', 'Minotaur', + 'Mite', 'Mole', 'Monger', 'Mongoose', 'Monk', 'Monkey', 'Moonfolk', 'Mount', 'Mouse', 'Mutant', 'Myr', + 'Mystic', 'Naga', 'Nautilus', 'Necron', 'Nephilim', 'Nightmare', 'Nightstalker', 'Ninja', 'Noble', 'Noggle', + 'Nomad', 'Nymph', 'Octopus', 'Ogre', 'Ooze', 'Orb', 'Orc', 'Orgg', 'Otter', 'Ouphe', 'Ox', 'Oyster', 'Pangolin', + 'Peasant', 'Pegasus', 'Pentavite', 'Performer', 'Pest', 'Phelddagrif', 'Phoenix', 'Phyrexian', 'Pilot', + 'Pincher', 'Pirate', 'Plant', 'Porcupine', 'Possum', 'Praetor', 'Primarch', 'Prism', 'Processor', 'Rabbit', + 'Raccoon', 'Ranger', 'Rat', 'Rebel', 'Reflection', 'Reveler', 'Rhino', 'Rigger', 'Robot', 'Rogue', 'Rukh', + 'Sable', 'Salamander', 'Samurai', 'Sand', 'Saproling', 'Satyr', 'Scarecrow', 'Scientist', 'Scion', 'Scorpion', + 'Scout', 'Sculpture', 'Serf', 'Serpent', 'Servo', 'Shade', 'Shaman', 'Shapeshifter', 'Shark', 'Sheep', 'Siren', + 'Skeleton', 'Skunk', 'Slith', 'Sliver', 'Sloth', 'Slug', 'Snail', 'Snake', 'Soldier', 'Soltari', 'Spawn', + 'Specter', 'Spellshaper', 'Sphinx', 'Spider', 'Spike', 'Spirit', 'Splinter', 'Sponge', 'Spy', 'Squid', + 'Squirrel', 'Starfish', 'Surrakar', 'Survivor', 'Synth', 'Teddy', 'Tentacle', 'Tetravite', 'Thalakos', + 'Thopter', 'Thrull', 'Tiefling', 'Time Lord', 'Toy', 'Treefolk', 'Trilobite', 'Triskelavite', 'Troll', + 'Turtle', 'Tyranid', 'Unicorn', 'Urzan', 'Vampire', 'Varmint', 'Vedalken', 'Volver', 'Wall', 'Walrus', + 'Warlock', 'Warrior', 'Wasp', 'Weasel', 'Weird', 'Werewolf', 'Whale', 'Wizard', 'Wolf', 'Wolverine', 'Wombat', + 'Worm', 'Wraith', 'Wurm', 'Yeti', 'Zombie', 'Zubera'] + +NON_CREATURE_TYPES: List[str] = ['Legendary', 'Creature', 'Enchantment', 'Artifact', + 'Battle', 'Sorcery', 'Instant', 'Land', '-', '—', + 'Blood', 'Clue', 'Food', 'Gold', 'Incubator', + 'Junk', 'Map', 'Powerstone', 'Treasure', + 'Equipment', 'Fortification', 'vehicle', + 'Bobblehead', 'Attraction', 'Contraption', + 'Siege', + 'Aura', 'Background', 'Saga', 'Role', 'Shard', + 'Cartouche', 'Case', 'Class', 'Curse', 'Rune', + 'Shrine', + 'Plains', 'Island', 'Swamp', 'Forest', 'Mountain', + 'Cave', 'Desert', 'Gate', 'Lair', 'Locus', 'Mine', + 'Power-Plant', 'Sphere', 'Tower', 'Urza\'s'] + +OUTLAW_TYPES: List[str] = ['Assassin', 'Mercenary', 'Pirate', 'Rogue', 'Warlock'] + +ENCHANTMENT_TOKENS: List[str] = ['Cursed Role', 'Monster Role', 'Royal Role', 'Sorcerer Role', + 'Virtuous Role', 'Wicked Role', 'Young Hero Role', 'Shard'] +ARTIFACT_TOKENS: List[str] = ['Blood', 'Clue', 'Food', 'Gold', 'Incubator', + 'Junk','Map','Powerstone', 'Treasure'] + +# Constants for DataFrame validation and processing +REQUIRED_COLUMNS: List[str] = [ + 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', + 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', + 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' +] + +# Mapping of card types to their corresponding theme tags +TYPE_TAG_MAPPING: List[str] = { + 'Artifact': ['Artifacts Matter'], + 'Battle': ['Battles Matter'], + #'Creature': [], + 'Enchantment': ['Enchantments Matter'], + 'Equipment': ['Equipment', 'Voltron'], + 'Aura': ['Auras', 'Voltron'], + 'Instant': ['Spells Matter', 'Spellslinger'], + 'Land': ['Lands Matter'], + 'Planeswalker': ['Superfriends'], + 'Sorcery': ['Spells Matter', 'Spellslinger'] +} + +# Constants for draw-related functionality +DRAW_RELATED_TAGS: List[str] = [ + 'Card Draw', # General card draw effects + 'Conditional Draw', # Draw effects with conditions/triggers + 'Cycling', # Cycling and similar discard-to-draw effects + 'Life to Draw', # Draw effects that require paying life + 'Loot', # Draw + discard effects + 'Replacement Draw', # Effects that modify or replace draws + 'Sacrifice to Draw', # Draw effects requiring sacrificing permanents + 'Unconditional Draw' # Pure card draw without conditions +] + +# Text patterns that exclude cards from being tagged as unconditional draw +DRAW_EXCLUSION_PATTERNS: List[str] = [ + 'annihilator', # Eldrazi mechanic that can match 'draw' patterns + 'ravenous', # Keyword that can match 'draw' patterns +] + +# Equipment-related constants +EQUIPMENT_EXCLUSIONS: List[str] = [ + 'Bruenor Battlehammer', # Equipment cost reduction + 'Nazahn, Revered Bladesmith', # Equipment tutor + 'Stonehewer Giant', # Equipment tutor +] + +EQUIPMENT_SPECIFIC_CARDS: List[str] = [ + 'Ardenn, Intrepid Archaeologist', # Equipment movement + 'Armory Automaton', # Mass equip ability + 'Brass Squire', # Free equip ability + 'Danitha Capashen, Paragon', # Equipment cost reduction + 'Halvar, God of Battle', # Equipment movement + 'Kemba, Kha Regent', # Equipment payoff + 'Kosei, Penitent Warlord', # Wants to be eequipped + 'Puresteel Paladin', # Equipment draw engine + 'Reyav, Master Smith', # Equipment combat boost + 'Sram, Senior Edificer', # Equipment card draw + 'Valduk, Keeper of the Flame' # Equipment token creation +] + +EQUIPMENT_RELATED_TAGS: List[str] = [ + 'Equipment', # Base equipment tag + 'Equipment Matters', # Cards that care about equipment + 'Voltron', # Commander-focused equipment strategy + 'Artifacts Matter', # Equipment are artifacts + 'Warriors Matter', # Common equipment tribal synergy + 'Knights Matter' # Common equipment tribal synergy +] + +EQUIPMENT_TEXT_PATTERNS: List[str] = [ + 'attach', # Equipment attachment + 'equip', # Equipment keyword + 'equipped', # Equipment state + 'equipment', # Equipment type + 'unattach', # Equipment removal + 'unequip', # Equipment removal +] + +# Aura-related constants +AURA_SPECIFIC_CARDS: List[str] = [ + 'Ardenn, Intrepid Archaeologist', # Aura movement + 'Calix, Guided By Fate', # Create duplicate Auras + 'Gilwain, Casting Director', # Creates role tokens + 'Ivy, Gleeful Spellthief', # Copies spells that have single target + 'Killian, Ink Duelist', # Targetted spell cost reduction +] + +# Constants for Voltron strategy +VOLTRON_COMMANDER_CARDS: List[str] = [ + 'Akiri, Line-Slinger', + 'Ardenn, Intrepid Archaeologist', + 'Bruna, Light of Alabaster', + 'Danitha Capashen, Paragon', + 'Greven, Predator Captain', + 'Halvar, God of Battle', + 'Kaldra Compleat', + 'Kemba, Kha Regent', + 'Light-Paws, Emperor\'s Voice', + 'Nahiri, the Lithomancer', + 'Rafiq of the Many', + 'Reyav, Master Smith', + 'Rograkh, Son of Rohgahh', + 'Sram, Senior Edificer', + 'Syr Gwyn, Hero of Ashvale', + 'Tiana, Ship\'s Caretaker', + 'Uril, the Miststalker', + 'Valduk, Keeper of the Flame', + 'Wyleth, Soul of Steel' +] + +VOLTRON_PATTERNS: List[str] = [ + 'attach', + 'aura you control', + 'enchant creature', + 'enchanted creature', + 'equipped creature', + 'equipment you control', + 'fortify', + 'living weapon', + 'reconfigure' +] + +# Constants for lands matter functionality +LANDS_MATTER_PATTERNS: Dict[str, List[str]] = { + 'land_play': [ + 'play a land', + 'play an additional land', + 'play two additional lands', + 'play lands from', + 'put a land card', + 'put a basic land card' + ], + 'land_search': [ + 'search your library for a basic land card', + 'search your library for a land card', + 'search your library for up to two basic land', + 'search their library for a basic land card' + ], + 'land_state': [ + 'land enters', + 'land card is put into your graveyard', + 'number of lands you control', + 'one or more land cards', + 'sacrifice a land', + 'target land' + ] +} + +DOMAIN_PATTERNS: List[str] = { + 'keyword': ['domain'], + 'text': ['basic land types among lands you control'] +} + +LANDFALL_PATTERNS: List[str] = { + 'keyword': ['landfall'], + 'triggers': [ + 'whenever a land enters the battlefield under your control', + 'when a land enters the battlefield under your control' + ] +} + +LANDWALK_PATTERNS: List[str] = { + 'basic': [ + 'plainswalker', + 'islandwalk', + 'swampwalk', + 'mountainwalk', + 'forestwalk' + ], + 'nonbasic': [ + 'nonbasic landwalk', + 'landwalk' + ] +} + +LAND_TYPES: List[str] = [ + # Basic lands + 'Plains', 'Island', 'Swamp', 'Mountain', 'Forest', + # Special lands + 'Cave', 'Desert', 'Gate', 'Lair', 'Locus', 'Mine', + 'Power-Plant', 'Sphere', 'Tower', 'Urza\'s' +] + +LANDS_MATTER_SPECIFIC_CARDS: List[str] = [ + 'Abundance', + 'Archdruid\'s Charm', + 'Archelos, Lagoon Mystic', + 'Catacylsmic Prospecting', + 'Coiling Oracle', + 'Disorienting Choice', + 'Eerie Ultimatum', + 'Gitrog Monster', + 'Mana Reflection', + 'Nahiri\'s Lithoforming', + 'Nine-fingers Keene', + 'Open the Way', + 'Realms Uncharted', + 'Reshape the Earth', + 'Scapeshift', + 'Yarok, the Desecrated', + 'Wonderscape Sage' +] + +# Constants for aristocrats functionality +ARISTOCRAT_TEXT_PATTERNS: List[str] = [ + 'another creature dies', + 'creature dies', + 'creature dying', + 'creature you control dies', + 'creature you own dies', + 'dies this turn', + 'dies, create', + 'dies, draw', + 'dies, each opponent', + 'dies, exile', + 'dies, put', + 'dies, return', + 'dies, sacrifice', + 'dies, you', + 'has blitz', + 'have blitz', + 'permanents were sacrificed', + 'sacrifice a creature', + 'sacrifice another', + 'sacrifice another creature', + 'sacrifice a nontoken', + 'sacrifice a permanent', + 'sacrifice another nontoken', + 'sacrifice another permanent', + 'sacrifice another token', + 'sacrifices a creature', + 'sacrifices another', + 'sacrifices another creature', + 'sacrifices another nontoken', + 'sacrifices another permanent', + 'sacrifices another token', + 'sacrifices a nontoken', + 'sacrifices a permanent', + 'sacrifices a token', + 'when this creature dies', + 'whenever a food', + 'whenever you sacrifice' +] + +ARISTOCRAT_SPECIFIC_CARDS: List[str] = [ + 'Ashnod, Flesh Mechanist', + 'Blood Artist', + 'Butcher of Malakir', + 'Chatterfang, Squirrel General', + 'Cruel Celebrant', + 'Dictate of Erebos', + 'Endrek Sahr, Master Breeder', + 'Gisa, Glorious Resurrector', + 'Grave Pact', + 'Grim Haruspex', + 'Judith, the Scourge Diva', + 'Korvold, Fae-Cursed King', + 'Mayhem Devil', + 'Midnight Reaper', + 'Mikaeus, the Unhallowed', + 'Pitiless Plunderer', + 'Poison-Tip Archer', + 'Savra, Queen of the Golgari', + 'Sheoldred, the Apocalypse', + 'Syr Konrad, the Grim', + 'Teysa Karlov', + 'Viscera Seer', + 'Yawgmoth, Thran Physician', + 'Zulaport Cutthroat' +] + +ARISTOCRAT_EXCLUSION_PATTERNS: List[str] = [ + 'blocking enchanted', + 'blocking it', + 'blocked by', + 'end the turn', + 'from your graveyard', + 'from your hand', + 'from your library', + 'into your hand' +] + +# Constants for stax functionality +STAX_TEXT_PATTERNS: List[str] = [ + 'an opponent controls' + 'can\'t attack', + 'can\'t be cast', + 'can\'t be activated', + 'can\'t cast spells', + 'can\'t enter', + 'can\'t search', + 'can\'t untap', + 'don\'t untap', + 'don\'t cause abilities', + 'each other player\'s', + 'each player\'s upkeep', + 'opponent would search', + 'opponents cast cost', + 'opponents can\'t', + 'opponents control', + 'opponents control can\'t', + 'opponents control enter tapped', + 'spells cost {1} more', + 'spells cost {2} more', + 'spells cost {3} more', + 'spells cost {4} more', + 'spells cost {5} more', + 'that player doesn\'t', + 'unless that player pays', + 'you control your opponent', + 'you gain protection' +] + +STAX_SPECIFIC_CARDS: List[str] = [ + 'Archon of Emeria', + 'Drannith Magistrate', + 'Ethersworn Canonist', + 'Grand Arbiter Augustin IV', + 'Hokori, Dust Drinker', + 'Kataki, War\'s Wage', + 'Lavinia, Azorius Renegade', + 'Leovold, Emissary of Trest', + 'Magus of the Moon', + 'Narset, Parter of Veils', + 'Opposition Agent', + 'Rule of Law', + 'Sanctum Prelate', + 'Thalia, Guardian of Thraben', + 'Winter Orb' +] + +STAX_EXCLUSION_PATTERNS: List[str] = [ + 'blocking enchanted', + 'blocking it', + 'blocked by', + 'end the turn', + 'from your graveyard', + 'from your hand', + 'from your library', + 'into your hand' +] + +# Constants for removal functionality +REMOVAL_TEXT_PATTERNS: List[str] = [ + 'destroy target', + 'destroys target', + 'exile target', + 'exiles target', + 'sacrifices target', + 'return target.*to.*hand', + 'returns target.*to.*hand' +] + +REMOVAL_SPECIFIC_CARDS: List[str] = ['from.*graveyard.*hand'] + +REMOVAL_EXCLUSION_PATTERNS: List[str] = [] + +REMOVAL_KEYWORDS: List[str] = [] + +# Constants for counterspell functionality +COUNTERSPELL_TEXT_PATTERNS: List[str] = [ + 'control counters a', + 'counter target', + 'counter that spell', + 'counter all', + 'counter each', + 'counter the next', + 'counters a spell', + 'counters target', + 'return target spell', + 'exile target spell', + 'counter unless', + 'unless its controller pays' +] + +COUNTERSPELL_SPECIFIC_CARDS: List[str] = [ + 'Arcane Denial', + 'Counterspell', + "Dovin's Veto", + 'Force of Will', + 'Mana Drain', + 'Mental Misstep', + 'Mindbreak Trap', + 'Mystic Confluence', + 'Pact of Negation', + 'Swan Song' +] + +COUNTERSPELL_EXCLUSION_PATTERNS: List[str] = [ + 'counter on', + 'counter from', + 'remove a counter', + 'move a counter', + 'distribute counter', + 'proliferate' +] + +# Constants for theft functionality +THEFT_TEXT_PATTERNS: List[str] = [ + 'cast a spell you don\'t own', + 'cast but don\'t own', + 'cost to cast this spell, sacrifice', + 'control but don\'t own', + 'exile top of target player\'s library', + 'exile top of each player\'s library', + 'gain control of', + 'target opponent\'s library', + 'that player\'s library', + 'you control enchanted creature' +] + +THEFT_SPECIFIC_CARDS: List[str] = [ + 'Adarkar Valkyrie', + 'Captain N\'gathrod', + 'Hostage Taker', + 'Siphon Insight', + 'Thief of Sanity', + 'Xanathar, Guild Kingpin', + 'Zara, Renegade Recruiter' +] + +# Constants for big mana functionality +BIG_MANA_TEXT_PATTERNS: List[str] = [ + 'add {w}{u}{b}{r}{g}', + 'card onto the battlefield', + 'control with power [3-5] or greater', + 'creature with power [3-5] or greater', + 'double the power', + 'from among them onto the battlefield', + 'from among them without paying', + 'hand onto the battlefield', + 'mana, add one mana', + 'mana, it produces twice', + 'mana, it produces three', + 'mana, its controller adds', + 'pay {w}{u}{b}{r}{g}', + 'spell with power 5 or greater', + 'value [5-7] or greater', + 'you may cast it without paying' +] + +BIG_MANA_SPECIFIC_CARDS: List[str] = [ + 'Akroma\'s Memorial', + 'Apex Devastator', + 'Apex of Power', + 'Brass\'s Bounty', + 'Cabal Coffers', + 'Caged Sun', + 'Doubling Cube', + 'Forsaken Monument', + 'Guardian Project', + 'Mana Reflection', + 'Nyxbloom Ancient', + 'Omniscience', + 'One with the Multiverse', + 'Portal to Phyrexia', + 'Vorinclex, Voice of Hunger' +] + +BIG_MANA_KEYWORDS: List[str] = [ + 'Cascade', + 'Convoke', + 'Discover', + 'Emerge', + 'Improvise', + 'Surge' +] + +# Constants for board wipe effects +BOARD_WIPE_TEXT_PATTERNS: Dict[str, List[str]] = { + 'mass_destruction': [ + 'destroy all', + 'destroy each', + 'destroy the rest', + 'destroys all', + 'destroys each', + 'destroys the rest' + ], + 'mass_exile': [ + 'exile all', + 'exile each', + 'exile the rest', + 'exiles all', + 'exiles each', + 'exiles the rest' + ], + 'mass_bounce': [ + 'return all', + 'return each', + 'put all creatures', + 'returns all', + 'returns each', + 'puts all creatures' + ], + 'mass_sacrifice': [ + 'sacrifice all', + 'sacrifice each', + 'sacrifice the rest', + 'sacrifices all', + 'sacrifices each', + 'sacrifices the rest' + ], + 'mass_damage': [ + 'deals damage to each', + 'deals damage to all', + 'deals X damage to each', + 'deals X damage to all', + 'deals that much damage to each', + 'deals that much damage to all' + ] +} + +BOARD_WIPE_SPECIFIC_CARDS: List[str] = [ + 'Akroma\'s Vengeance', + 'All Is Dust', + 'Austere Command', + 'Blasphemous Act', + 'Cleansing Nova', + 'Cyclonic Rift', + 'Damnation', + 'Day of Judgment', + 'Decree of Pain', + 'Devastation Tide', + 'Evacuation', + 'Extinction Event', + 'Farewell', + 'Hour of Devastation', + 'In Garruk\'s Wake', + 'Living Death', + 'Living End', + 'Merciless Eviction', + 'Nevinyrral\'s Disk', + 'Oblivion Stone', + 'Planar Cleansing', + 'Ravnica at War', + 'Shatter the Sky', + 'Supreme Verdict', + 'Terminus', + 'Time Wipe', + 'Toxic Deluge', + 'Vanquish the Horde', + 'Wrath of God' +] + +BOARD_WIPE_EXCLUSION_PATTERNS: List[str] = [ + 'blocking enchanted', + 'blocking it', + 'blocked by', + 'end the turn', + 'from your graveyard', + 'from your hand', + 'from your library', + 'into your hand', + 'target player\'s library', + 'that player\'s library' +] + +# Constants for topdeck manipulation +TOPDECK_TEXT_PATTERNS: List[str] = [ + 'from the top', + 'look at the top', + 'reveal the top', + 'scries', + 'surveils', + 'top of your library', + 'you scry', + 'you surveil' +] + +TOPDECK_KEYWORDS: List[str] = [ + 'Miracle', + 'Scry', + 'Surveil' +] + +TOPDECK_SPECIFIC_CARDS: List[str] = [ + 'Aminatou, the Fateshifter', + 'Brainstorm', + 'Counterbalance', + 'Delver of Secrets', + 'Jace, the Mind Sculptor', + 'Lantern of Insight', + 'Melek, Izzet Paragon', + 'Mystic Forge', + 'Sensei\'s Divining Top', + 'Soothsaying', + 'Temporal Mastery', + 'Vampiric Tutor' +] + +TOPDECK_EXCLUSION_PATTERNS: List[str] = [ + 'from the top of target player\'s library', + 'from the top of their library', + 'look at the top card of target player\'s library', + 'reveal the top card of target player\'s library' +] \ No newline at end of file diff --git a/tag_utils.py b/code/tagging/tag_utils.py similarity index 98% rename from tag_utils.py rename to code/tagging/tag_utils.py index 4da013a..8374b96 100644 --- a/tag_utils.py +++ b/code/tagging/tag_utils.py @@ -22,7 +22,8 @@ from typing import List, Set, Union, Any import pandas as pd # Local application imports -import settings +from . import tag_constants + def pluralize(word: str) -> str: """Convert a word to its plural form using basic English pluralization rules. @@ -319,10 +320,10 @@ def create_mass_effect_mask(df: pd.DataFrame, effect_type: str) -> pd.Series[boo Raises: ValueError: If effect_type is not recognized """ - if effect_type not in settings.BOARD_WIPE_TEXT_PATTERNS: + if effect_type not in tag_constants.BOARD_WIPE_TEXT_PATTERNS: raise ValueError(f"Unknown effect type: {effect_type}") - patterns = settings.BOARD_WIPE_TEXT_PATTERNS[effect_type] + patterns = tag_constants.BOARD_WIPE_TEXT_PATTERNS[effect_type] return create_text_mask(df, patterns) def create_damage_pattern(number: Union[int, str]) -> str: diff --git a/tagger.py b/code/tagging/tagger.py similarity index 96% rename from tagger.py rename to code/tagging/tagger.py index 877e518..750ad6c 100644 --- a/tagger.py +++ b/code/tagging/tagger.py @@ -1,7 +1,6 @@ from __future__ import annotations # Standard library imports -import logging import os import re from typing import Union @@ -9,65 +8,18 @@ from typing import Union # Third-party imports import pandas as pd -import settings -import tag_utils - # Local application imports -from settings import CSV_DIRECTORY, multiple_copy_cards, num_to_search, triggers -from setup import regenerate_csv_by_color - - -# Constants for common tag groupings -TAG_GROUPS = { - "Cantrips": ["Cantrips", "Card Draw", "Spellslinger", "Spells Matter"], - "Tokens": ["Token Creation", "Tokens Matter"], - "Counters": ["Counters Matter"], - "Combat": ["Combat Matters", "Combat Tricks"], - "Artifacts": ["Artifacts Matter", "Artifact Tokens"], - "Enchantments": ["Enchantments Matter", "Enchantment Tokens"], - "Lands": ["Lands Matter"], - "Spells": ["Spellslinger", "Spells Matter"] -} - -# Common regex patterns -PATTERN_GROUPS = { - "draw": r"draw[s]? a card|draw[s]? one card", - "combat": r"attack[s]?|block[s]?|combat damage", - "tokens": r"create[s]? .* token|put[s]? .* token", - "counters": r"\+1/\+1 counter|\-1/\-1 counter|loyalty counter", - "sacrifice": r"sacrifice[s]? .*|sacrificed", - "exile": r"exile[s]? .*|exiled", - "cost_reduction": r"cost[s]? \{[\d\w]\} less|affinity for|cost[s]? less to cast|chosen type cost|copy cost|from exile cost|from exile this turn cost|from your graveyard cost|has undaunted|have affinity for artifacts|other than your hand cost|spells cost|spells you cast cost|that target .* cost|those spells cost|you cast cost|you pay cost" -} - -# Create logs directory if it doesn't exist -if not os.path.exists('logs'): - os.makedirs('logs') - -# Logging configuration -LOG_DIR = 'logs' -LOG_FILE = f'{LOG_DIR}/tagger.log' -LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' -LOG_LEVEL = logging.INFO - -# Create formatters and handlers -formatter = logging.Formatter(LOG_FORMAT) - -# File handler -file_handler = logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8') -file_handler.setFormatter(formatter) - -# Stream handler -stream_handler = logging.StreamHandler() -stream_handler.setFormatter(formatter) +from . import tag_utils +from . import tag_constants +from settings import CSV_DIRECTORY, MULTIPLE_COPY_CARDS, COLORS +import logging_util +from file_setup import setup # Create logger for this module -logger = logging.getLogger(__name__) -logger.setLevel(LOG_LEVEL) - -# Add handlers to logger -logger.addHandler(file_handler) -logger.addHandler(stream_handler) +logger = logging_util.logging.getLogger(__name__) +logger.setLevel(logging_util.LOG_LEVEL) +logger.addHandler(logging_util.file_handler) +logger.addHandler(logging_util.stream_handler) ### Setup ## Load the dataframe @@ -88,7 +40,7 @@ def load_dataframe(color: str) -> None: # Check if file exists, regenerate if needed if not os.path.exists(filepath): logger.warning(f'{color}_cards.csv not found, regenerating it.') - regenerate_csv_by_color(color) + setup.regenerate_csv_by_color(color) if not os.path.exists(filepath): raise FileNotFoundError(f"Failed to generate {filepath}") @@ -213,8 +165,8 @@ def kindred_tagging(df: pd.DataFrame, color: str) -> None: for idx, row in creature_rows.iterrows(): types = tag_utils.extract_creature_types( row['type'], - settings.creature_types, - settings.non_creature_types + tag_constants.CREATURE_TYPES, + tag_constants.NON_CREATURE_TYPES ) if types: df.at[idx, 'creatureTypes'] = types @@ -225,7 +177,7 @@ def kindred_tagging(df: pd.DataFrame, color: str) -> None: logger.info(f'Setting Outlaw creature type tags on {color}_cards.csv') # Process outlaw types - outlaws = settings.OUTLAW_TYPES + outlaws = tag_constants.OUTLAW_TYPES df['creatureTypes'] = df.apply( lambda row: tag_utils.add_outlaw_type(row['creatureTypes'], outlaws) if isinstance(row['creatureTypes'], list) else row['creatureTypes'], @@ -249,7 +201,7 @@ def kindred_tagging(df: pd.DataFrame, color: str) -> None: text_types = tag_utils.find_types_in_text( row['text'], row['name'], - settings.creature_types + tag_constants.CREATURE_TYPES ) if text_types: current_types = row['creatureTypes'] @@ -270,7 +222,7 @@ def kindred_tagging(df: pd.DataFrame, color: str) -> None: 'keywords', 'layout', 'side' ] df = df[columns_to_keep] - df.to_csv(f'{settings.CSV_DIRECTORY}/{color}_cards.csv', index=False) + df.to_csv(f'{tag_constants.CSV_DIRECTORY}/{color}_cards.csv', index=False) total_time = pd.Timestamp.now() - start_time logger.info(f'Creature type tagging completed in {total_time.total_seconds():.2f}s') @@ -308,7 +260,7 @@ def create_theme_tags(df: pd.DataFrame, color: str) -> None: raise TypeError("df must be a pandas DataFrame") if not isinstance(color, str): raise TypeError("color must be a string") - if color not in settings.COLORS: + if color not in COLORS: raise ValueError(f"Invalid color: {color}") try: @@ -327,7 +279,7 @@ def create_theme_tags(df: pd.DataFrame, color: str) -> None: raise ValueError(f"Missing required columns: {missing}") # Define column order - columns_to_keep = settings.REQUIRED_COLUMNS + columns_to_keep = tag_constants.REQUIRED_COLUMNS # Reorder columns efficiently available_cols = [col for col in columns_to_keep if col in df.columns] @@ -335,7 +287,7 @@ def create_theme_tags(df: pd.DataFrame, color: str) -> None: # Save results try: - df.to_csv(f'{settings.CSV_DIRECTORY}/{color}_cards.csv', index=False) + df.to_csv(f'{CSV_DIRECTORY}/{color}_cards.csv', index=False) total_time = pd.Timestamp.now() - start_time logger.info(f'Creature type tagging completed in {total_time.total_seconds():.2f}s') @@ -375,7 +327,7 @@ def tag_for_card_types(df: pd.DataFrame, color: str) -> None: raise ValueError(f"Missing required columns: {required_cols - set(df.columns)}") # Define type-to-tag mapping - type_tag_map = settings.TYPE_TAG_MAPPING + type_tag_map = tag_constants.TYPE_TAG_MAPPING # Process each card type for card_type, tags in type_tag_map.items(): @@ -518,7 +470,7 @@ def tag_for_cost_reduction(df: pd.DataFrame, color: str) -> None: try: # Create masks for different cost reduction patterns - cost_mask = tag_utils.create_text_mask(df, PATTERN_GROUPS['cost_reduction']) + cost_mask = tag_utils.create_text_mask(df, tag_constants.PATTERN_GROUPS['cost_reduction']) # Add specific named cards named_cards = [ @@ -634,15 +586,15 @@ def create_unconditional_draw_mask(df: pd.DataFrame) -> pd.Series: Boolean Series indicating which cards have unconditional draw effects """ # Create pattern for draw effects using num_to_search - draw_patterns = [f'draw {num} card' for num in num_to_search] + draw_patterns = [f'draw {num} card' for num in tag_constants.NUM_TO_SEARCH] draw_mask = tag_utils.create_text_mask(df, draw_patterns) # Create exclusion mask for conditional effects - excluded_tags = settings.DRAW_RELATED_TAGS + excluded_tags = tag_constants.DRAW_RELATED_TAGS tag_mask = tag_utils.create_tag_mask(df, excluded_tags) # Create text-based exclusions - text_patterns = settings.DRAW_EXCLUSION_PATTERNS + text_patterns = tag_constants.DRAW_EXCLUSION_PATTERNS text_mask = tag_utils.create_text_mask(df, text_patterns) return draw_mask & ~(tag_mask | text_mask) @@ -687,11 +639,11 @@ def create_conditional_draw_exclusion_mask(df: pd.DataFrame) -> pd.Series: Boolean Series indicating which cards should be excluded """ # Create tag-based exclusions - excluded_tags = settings.DRAW_RELATED_TAGS + excluded_tags = tag_constants.DRAW_RELATED_TAGS tag_mask = tag_utils.create_tag_mask(df, excluded_tags) # Create text-based exclusions - text_patterns = settings.DRAW_EXCLUSION_PATTERNS + ['whenever you draw a card'] + text_patterns = tag_constants.DRAW_EXCLUSION_PATTERNS + ['whenever you draw a card'] text_mask = tag_utils.create_text_mask(df, text_patterns) # Create name-based exclusions @@ -711,7 +663,7 @@ def create_conditional_draw_trigger_mask(df: pd.DataFrame) -> pd.Series: """ # Build trigger patterns trigger_patterns = [] - for trigger in triggers: + for trigger in tag_constants.TRIGGERS: # Permanent/creature/player triggers trigger_patterns.extend([ f'{trigger} a permanent', @@ -747,7 +699,7 @@ def create_conditional_draw_effect_mask(df: pd.DataFrame) -> pd.Series: Boolean Series indicating which cards have draw effects """ # Create draw patterns using num_to_search - draw_patterns = [f'draw {num} card' for num in num_to_search] + draw_patterns = [f'draw {num} card' for num in tag_constants.NUM_TO_SEARCH] # Add token and 'draw for each' patterns draw_patterns.extend([ @@ -787,7 +739,7 @@ def tag_for_conditional_draw(df: pd.DataFrame, color: str) -> None: trigger_mask = create_conditional_draw_trigger_mask(df) # Create draw effect mask - draw_patterns = [f'draw {num} card' for num in num_to_search] + draw_patterns = [f'draw {num} card' for num in tag_constants.NUM_TO_SEARCH] # Add token and 'draw for each' patterns draw_patterns.extend([ @@ -824,7 +776,7 @@ def create_loot_mask(df: pd.DataFrame) -> pd.Series: has_other_loot = tag_utils.create_tag_mask(df, ['Cycling', 'Connive']) | df['text'].str.contains('blood token', case=False, na=False) # Match draw + discard patterns - draw_patterns = [f'draw {num} card' for num in num_to_search] + draw_patterns = [f'draw {num} card' for num in tag_constants.NUM_TO_SEARCH] discard_patterns = [ 'discard the rest', 'for each card drawn this way, discard', @@ -959,7 +911,7 @@ def create_replacement_draw_mask(df: pd.DataFrame) -> pd.Series: """ # Create trigger patterns trigger_patterns = [] - for trigger in triggers: + for trigger in tag_constants.TRIGGERS: trigger_patterns.extend([ f'{trigger} a player.*instead.*draw', f'{trigger} an opponent.*instead.*draw', @@ -981,7 +933,7 @@ def create_replacement_draw_mask(df: pd.DataFrame) -> pd.Series: base_mask = tag_utils.create_text_mask(df, all_patterns) # Add mask for specific card numbers - number_patterns = [f'draw {num} card' for num in num_to_search] + number_patterns = [f'draw {num} card' for num in tag_constants.NUM_TO_SEARCH] number_mask = tag_utils.create_text_mask(df, number_patterns) # Add mask for non-specific numbers @@ -999,11 +951,11 @@ def create_replacement_draw_exclusion_mask(df: pd.DataFrame) -> pd.Series: Boolean Series indicating which cards should be excluded """ # Create tag-based exclusions - excluded_tags = settings.DRAW_RELATED_TAGS + excluded_tags = tag_constants.DRAW_RELATED_TAGS tag_mask = tag_utils.create_tag_mask(df, excluded_tags) # Create text-based exclusions - text_patterns = settings.DRAW_EXCLUSION_PATTERNS + ['skips that turn instead'] + text_patterns = tag_constants.DRAW_EXCLUSION_PATTERNS + ['skips that turn instead'] text_mask = tag_utils.create_text_mask(df, text_patterns) return tag_mask | text_mask @@ -1114,7 +1066,7 @@ def tag_for_wheels(df: pd.DataFrame, color: str) -> None: tag_utils.apply_tag_vectorized(df, final_mask, ['Card Draw', 'Wheels']) # Add Draw Triggers tag for cards with trigger words - trigger_pattern = '|'.join(triggers) + trigger_pattern = '|'.join(tag_constants.TRIGGERS) trigger_mask = final_mask & df['text'].str.contains(trigger_pattern, case=False, na=False) tag_utils.apply_tag_vectorized(df, trigger_mask, ['Draw Triggers']) @@ -1161,10 +1113,14 @@ def tag_for_artifacts(df: pd.DataFrame, color: str) -> None: required_cols = {'text', 'themeTags'} tag_utils.validate_dataframe_columns(df, required_cols) - # Process each type of draw effect + # Process each type of artifact effect tag_for_artifact_tokens(df, color) logger.info('Completed Artifact token tagging') print('\n==========\n') + + tag_for_artifact_triggers(df, color) + logger.info('Completed Artifact trigger tagging') + print('\n==========\n') tag_equipment(df, color) logger.info('Completed Equipment tagging') @@ -1314,7 +1270,7 @@ def create_predefined_artifact_mask(df: pd.DataFrame) -> tuple[pd.Series, dict[i # Create masks for each token type token_masks = [] - for token in settings.artifact_tokens: + for token in tag_constants.ARTIFACT_TOKENS: token_mask = tag_utils.create_text_mask(df, token.lower()) # Handle exclusions @@ -1493,7 +1449,7 @@ def create_equipment_cares_mask(df: pd.DataFrame) -> pd.Series: keyword_mask = tag_utils.create_keyword_mask(df, keyword_patterns) # Create specific cards mask - specific_cards = settings.EQUIPMENT_SPECIFIC_CARDS + specific_cards = tag_constants.EQUIPMENT_SPECIFIC_CARDS name_mask = tag_utils.create_name_mask(df, specific_cards) return text_mask | keyword_mask | name_mask @@ -1767,7 +1723,7 @@ def create_predefined_enchantment_mask(df: pd.DataFrame) -> pd.Series: # Create masks for each token type token_masks = [] - for token in settings.enchantment_tokens: + for token in tag_constants.ENCHANTMENT_TOKENS: token_mask = tag_utils.create_text_mask(df, token.lower()) token_masks.append(token_mask) @@ -1887,7 +1843,7 @@ def tag_auras(df: pd.DataFrame, color: str) -> None: 'aura you control enters', 'enchanted' ] - cares_mask = tag_utils.create_text_mask(df, text_patterns) | tag_utils.create_name_mask(df, settings.AURA_SPECIFIC_CARDS) + cares_mask = tag_utils.create_text_mask(df, text_patterns) | tag_utils.create_name_mask(df, tag_constants.AURA_SPECIFIC_CARDS) if cares_mask.any(): tag_utils.apply_tag_vectorized(df, cares_mask, ['Auras', 'Enchantments Matter', 'Voltron']) @@ -2793,8 +2749,8 @@ def tag_for_lifegain(df: pd.DataFrame, color: str) -> None: try: # Create masks for different lifegain patterns - gain_patterns = [f'gain {num} life' for num in settings.num_to_search] - gain_patterns.extend([f'gains {num} life' for num in settings.num_to_search]) + gain_patterns = [f'gain {num} life' for num in tag_constants.NUM_TO_SEARCH] + gain_patterns.extend([f'gains {num} life' for num in tag_constants.NUM_TO_SEARCH]) gain_patterns.extend(['gain life', 'gains life']) gain_mask = tag_utils.create_text_mask(df, gain_patterns) @@ -3144,7 +3100,7 @@ def tag_for_special_counters(df: pd.DataFrame, color: str) -> None: try: # Process each counter type counter_counts = {} - for counter_type in settings.counter_types: + for counter_type in tag_constants.COUNTER_TYPES: # Create pattern for this counter type pattern = f'{counter_type} counter' mask = tag_utils.create_text_mask(df, pattern) @@ -3177,7 +3133,7 @@ def create_voltron_commander_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards are Voltron commanders """ - return tag_utils.create_name_mask(df, settings.VOLTRON_COMMANDER_CARDS) + return tag_utils.create_name_mask(df, tag_constants.VOLTRON_COMMANDER_CARDS) def create_voltron_support_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for cards that support Voltron strategies. @@ -3188,7 +3144,7 @@ def create_voltron_support_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards support Voltron strategies """ - return tag_utils.create_text_mask(df, settings.VOLTRON_PATTERNS) + return tag_utils.create_text_mask(df, tag_constants.VOLTRON_PATTERNS) def create_voltron_equipment_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for Equipment-based Voltron cards. @@ -3283,12 +3239,12 @@ def create_lands_matter_mask(df: pd.DataFrame) -> pd.Series: Boolean Series indicating which cards have lands matter effects """ # Create mask for named cards - name_mask = tag_utils.create_name_mask(df, settings.LANDS_MATTER_SPECIFIC_CARDS) + name_mask = tag_utils.create_name_mask(df, tag_constants.LANDS_MATTER_SPECIFIC_CARDS) # Create text pattern masks - play_mask = tag_utils.create_text_mask(df, settings.LANDS_MATTER_PATTERNS['land_play']) - search_mask = tag_utils.create_text_mask(df, settings.LANDS_MATTER_PATTERNS['land_search']) - state_mask = tag_utils.create_text_mask(df, settings.LANDS_MATTER_PATTERNS['land_state']) + play_mask = tag_utils.create_text_mask(df, tag_constants.LANDS_MATTER_PATTERNS['land_play']) + search_mask = tag_utils.create_text_mask(df, tag_constants.LANDS_MATTER_PATTERNS['land_search']) + state_mask = tag_utils.create_text_mask(df, tag_constants.LANDS_MATTER_PATTERNS['land_state']) # Combine all masks return name_mask | play_mask | search_mask | state_mask @@ -3302,8 +3258,8 @@ def create_domain_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards have domain effects """ - keyword_mask = tag_utils.create_keyword_mask(df, settings.DOMAIN_PATTERNS['keyword']) - text_mask = tag_utils.create_text_mask(df, settings.DOMAIN_PATTERNS['text']) + keyword_mask = tag_utils.create_keyword_mask(df, tag_constants.DOMAIN_PATTERNS['keyword']) + text_mask = tag_utils.create_text_mask(df, tag_constants.DOMAIN_PATTERNS['text']) return keyword_mask | text_mask def create_landfall_mask(df: pd.DataFrame) -> pd.Series: @@ -3315,8 +3271,8 @@ def create_landfall_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards have landfall effects """ - keyword_mask = tag_utils.create_keyword_mask(df, settings.LANDFALL_PATTERNS['keyword']) - trigger_mask = tag_utils.create_text_mask(df, settings.LANDFALL_PATTERNS['triggers']) + keyword_mask = tag_utils.create_keyword_mask(df, tag_constants.LANDFALL_PATTERNS['keyword']) + trigger_mask = tag_utils.create_text_mask(df, tag_constants.LANDFALL_PATTERNS['triggers']) return keyword_mask | trigger_mask def create_landwalk_mask(df: pd.DataFrame) -> pd.Series: @@ -3328,8 +3284,8 @@ def create_landwalk_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards have landwalk abilities """ - basic_mask = tag_utils.create_text_mask(df, settings.LANDWALK_PATTERNS['basic']) - nonbasic_mask = tag_utils.create_text_mask(df, settings.LANDWALK_PATTERNS['nonbasic']) + basic_mask = tag_utils.create_text_mask(df, tag_constants.LANDWALK_PATTERNS['basic']) + nonbasic_mask = tag_utils.create_text_mask(df, tag_constants.LANDWALK_PATTERNS['nonbasic']) return basic_mask | nonbasic_mask def create_land_types_mask(df: pd.DataFrame) -> pd.Series: @@ -3342,11 +3298,11 @@ def create_land_types_mask(df: pd.DataFrame) -> pd.Series: Boolean Series indicating which cards care about specific land types """ # Create type-based mask - type_mask = tag_utils.create_type_mask(df, settings.LAND_TYPES) + type_mask = tag_utils.create_type_mask(df, tag_constants.LAND_TYPES) # Create text pattern masks for each land type text_masks = [] - for land_type in settings.LAND_TYPES: + for land_type in tag_constants.LAND_TYPES: patterns = [ f'search your library for a {land_type.lower()}', f'search your library for up to two {land_type.lower()}', @@ -3654,7 +3610,7 @@ def tag_for_cantrips(df: pd.DataFrame, color: str) -> None: excluded_names = df['name'].isin(EXCLUDED_NAMES) # Create cantrip condition masks - has_draw = tag_utils.create_text_mask(df, PATTERN_GROUPS['draw']) + has_draw = tag_utils.create_text_mask(df, tag_constants.PATTERN_GROUPS['draw']) low_cost = df['manaValue'].fillna(float('inf')) <= 2 # Combine conditions @@ -3668,7 +3624,7 @@ def tag_for_cantrips(df: pd.DataFrame, color: str) -> None: ) # Apply tags - tag_utils.apply_tag_vectorized(df, cantrip_mask, TAG_GROUPS['Cantrips']) + tag_utils.apply_tag_vectorized(df, cantrip_mask, tag_constants.TAG_GROUPS['Cantrips']) # Log results cantrip_count = cantrip_mask.sum() @@ -4169,7 +4125,7 @@ def create_aristocrat_text_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards have aristocrat text patterns """ - return tag_utils.create_text_mask(df, settings.ARISTOCRAT_TEXT_PATTERNS) + return tag_utils.create_text_mask(df, tag_constants.ARISTOCRAT_TEXT_PATTERNS) def create_aristocrat_name_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for specific aristocrat-related cards. @@ -4180,7 +4136,7 @@ def create_aristocrat_name_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards are specific aristocrat cards """ - return tag_utils.create_name_mask(df, settings.ARISTOCRAT_SPECIFIC_CARDS) + return tag_utils.create_name_mask(df, tag_constants.ARISTOCRAT_SPECIFIC_CARDS) def create_aristocrat_self_sacrifice_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for creatures with self-sacrifice effects. @@ -4225,7 +4181,7 @@ def create_aristocrat_exclusion_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards should be excluded """ - return tag_utils.create_text_mask(df, settings.ARISTOCRAT_EXCLUSION_PATTERNS) + return tag_utils.create_text_mask(df, tag_constants.ARISTOCRAT_EXCLUSION_PATTERNS) def tag_for_aristocrats(df: pd.DataFrame, color: str) -> None: """Tag cards that fit the Aristocrats or Sacrifice Matters themes using vectorized operations. @@ -4332,10 +4288,10 @@ def tag_for_big_mana(df: pd.DataFrame, color: str) -> None: tag_utils.validate_dataframe_columns(df, required_cols) # Create masks for different big mana patterns - text_mask = tag_utils.create_text_mask(df, settings.BIG_MANA_TEXT_PATTERNS) - keyword_mask = tag_utils.create_keyword_mask(df, settings.BIG_MANA_KEYWORDS) + text_mask = tag_utils.create_text_mask(df, tag_constants.BIG_MANA_TEXT_PATTERNS) + keyword_mask = tag_utils.create_keyword_mask(df, tag_constants.BIG_MANA_KEYWORDS) cost_mask = create_big_mana_cost_mask(df) - specific_mask = tag_utils.create_name_mask(df, settings.BIG_MANA_SPECIFIC_CARDS) + specific_mask = tag_utils.create_name_mask(df, tag_constants.BIG_MANA_SPECIFIC_CARDS) tag_mask = tag_utils.create_tag_mask(df, 'Cost Reduction') # Combine all masks @@ -5106,8 +5062,8 @@ def create_mill_text_mask(df: pd.DataFrame) -> pd.Series: text_mask = tag_utils.create_text_mask(df, text_patterns) # Create mill number patterns - mill_patterns = [f'mill {num}' for num in settings.num_to_search] - mill_patterns.extend([f'mills {num}' for num in settings.num_to_search]) + mill_patterns = [f'mill {num}' for num in tag_constants.NUM_TO_SEARCH] + mill_patterns.extend([f'mills {num}' for num in tag_constants.NUM_TO_SEARCH]) number_mask = tag_utils.create_text_mask(df, mill_patterns) return text_mask | number_mask @@ -5261,7 +5217,7 @@ def tag_for_multiple_copies(df: pd.DataFrame, color: str) -> None: tag_utils.validate_dataframe_columns(df, required_cols) # Create mask for multiple copy cards - multiple_copies_mask = tag_utils.create_name_mask(df, multiple_copy_cards) + multiple_copies_mask = tag_utils.create_name_mask(df, MULTIPLE_COPY_CARDS) # Apply tags if multiple_copies_mask.any(): @@ -5487,7 +5443,7 @@ def create_stax_text_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards have stax text patterns """ - return tag_utils.create_text_mask(df, settings.STAX_TEXT_PATTERNS) + return tag_utils.create_text_mask(df, tag_constants.STAX_TEXT_PATTERNS) def create_stax_name_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for cards used in stax strategies. @@ -5498,7 +5454,7 @@ def create_stax_name_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards have stax text patterns """ - return tag_utils.create_text_mask(df, settings.STAX_SPECIFIC_CARDS) + return tag_utils.create_text_mask(df, tag_constants.STAX_SPECIFIC_CARDS) def create_stax_tag_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for cards with stax-related tags. @@ -5521,7 +5477,7 @@ def create_stax_exclusion_mask(df: pd.DataFrame) -> pd.Series: Boolean Series indicating which cards should be excluded """ # Add specific exclusion patterns here if needed - return tag_utils.create_text_mask(df, settings.STAX_EXCLUSION_PATTERNS) + return tag_utils.create_text_mask(df, tag_constants.STAX_EXCLUSION_PATTERNS) def tag_for_stax(df: pd.DataFrame, color: str) -> None: """Tag cards that fit the Stax theme using vectorized operations. @@ -5577,7 +5533,7 @@ def create_theft_text_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards have theft text patterns """ - return tag_utils.create_text_mask(df, settings.THEFT_TEXT_PATTERNS) + return tag_utils.create_text_mask(df, tag_constants.THEFT_TEXT_PATTERNS) def create_theft_name_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for specific theft-related cards. @@ -5588,7 +5544,7 @@ def create_theft_name_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards are specific theft cards """ - return tag_utils.create_name_mask(df, settings.THEFT_SPECIFIC_CARDS) + return tag_utils.create_name_mask(df, tag_constants.THEFT_SPECIFIC_CARDS) def tag_for_theft(df: pd.DataFrame, color: str) -> None: """Tag cards that steal or use opponents' resources using vectorized operations. @@ -5751,7 +5707,7 @@ def create_topdeck_text_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards have topdeck text patterns """ - return tag_utils.create_text_mask(df, settings.TOPDECK_TEXT_PATTERNS) + return tag_utils.create_text_mask(df, tag_constants.TOPDECK_TEXT_PATTERNS) def create_topdeck_keyword_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for cards with topdeck-related keywords. @@ -5762,7 +5718,7 @@ def create_topdeck_keyword_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards have topdeck keywords """ - return tag_utils.create_keyword_mask(df, settings.TOPDECK_KEYWORDS) + return tag_utils.create_keyword_mask(df, tag_constants.TOPDECK_KEYWORDS) def create_topdeck_specific_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for specific topdeck-related cards. @@ -5773,7 +5729,7 @@ def create_topdeck_specific_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards are specific topdeck cards """ - return tag_utils.create_name_mask(df, settings.TOPDECK_SPECIFIC_CARDS) + return tag_utils.create_name_mask(df, tag_constants.TOPDECK_SPECIFIC_CARDS) def create_topdeck_exclusion_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for cards that should be excluded from topdeck effects. @@ -5784,7 +5740,7 @@ def create_topdeck_exclusion_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards should be excluded """ - return tag_utils.create_text_mask(df, settings.TOPDECK_EXCLUSION_PATTERNS) + return tag_utils.create_text_mask(df, tag_constants.TOPDECK_EXCLUSION_PATTERNS) def tag_for_topdeck(df: pd.DataFrame, color: str) -> None: """Tag cards that manipulate the top of library using vectorized operations. @@ -5990,7 +5946,7 @@ def create_counterspell_text_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards have counterspell text patterns """ - return tag_utils.create_text_mask(df, settings.COUNTERSPELL_TEXT_PATTERNS) + return tag_utils.create_text_mask(df, tag_constants.COUNTERSPELL_TEXT_PATTERNS) def create_counterspell_specific_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for specific counterspell cards. @@ -6001,7 +5957,7 @@ def create_counterspell_specific_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards are specific counterspell cards """ - return tag_utils.create_name_mask(df, settings.COUNTERSPELL_SPECIFIC_CARDS) + return tag_utils.create_name_mask(df, tag_constants.COUNTERSPELL_SPECIFIC_CARDS) def create_counterspell_exclusion_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for cards that should be excluded from counterspell effects. @@ -6012,7 +5968,7 @@ def create_counterspell_exclusion_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards should be excluded """ - return tag_utils.create_text_mask(df, settings.COUNTERSPELL_EXCLUSION_PATTERNS) + return tag_utils.create_text_mask(df, tag_constants.COUNTERSPELL_EXCLUSION_PATTERNS) def tag_for_counterspells(df: pd.DataFrame, color: str) -> None: """Tag cards that counter spells using vectorized operations. @@ -6101,10 +6057,10 @@ def tag_for_board_wipes(df: pd.DataFrame, color: str) -> None: damage_mask = tag_utils.create_mass_damage_mask(df) # Create exclusion mask - exclusion_mask = tag_utils.create_text_mask(df, settings.BOARD_WIPE_EXCLUSION_PATTERNS) + exclusion_mask = tag_utils.create_text_mask(df, tag_constants.BOARD_WIPE_EXCLUSION_PATTERNS) # Create specific cards mask - specific_mask = tag_utils.create_name_mask(df, settings.BOARD_WIPE_SPECIFIC_CARDS) + specific_mask = tag_utils.create_name_mask(df, tag_constants.BOARD_WIPE_SPECIFIC_CARDS) # Combine all masks final_mask = ( @@ -6407,7 +6363,7 @@ def create_removal_text_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards have removal text patterns """ - return tag_utils.create_text_mask(df, settings.REMOVAL_TEXT_PATTERNS) + return tag_utils.create_text_mask(df, tag_constants.REMOVAL_TEXT_PATTERNS) def create_removal_exclusion_mask(df: pd.DataFrame) -> pd.Series: """Create a boolean mask for cards that should be excluded from removal effects. @@ -6418,7 +6374,7 @@ def create_removal_exclusion_mask(df: pd.DataFrame) -> pd.Series: Returns: Boolean Series indicating which cards should be excluded """ - return tag_utils.create_text_mask(df, settings.REMOVAL_EXCLUSION_PATTERNS) + return tag_utils.create_text_mask(df, tag_constants.REMOVAL_EXCLUSION_PATTERNS) def tag_for_removal(df: pd.DataFrame, color: str) -> None: @@ -6474,7 +6430,7 @@ def tag_for_removal(df: pd.DataFrame, color: str) -> None: def run_tagging(): start_time = pd.Timestamp.now() - for color in settings.COLORS: + for color in COLORS: load_dataframe(color) duration = (pd.Timestamp.now() - start_time).total_seconds() logger.info(f'Tagged cards in {duration:.2f}s') diff --git a/type_definitions.py b/code/type_definitions.py similarity index 100% rename from type_definitions.py rename to code/type_definitions.py diff --git a/settings.py b/settings.py deleted file mode 100644 index 3a7be80..0000000 --- a/settings.py +++ /dev/null @@ -1,1376 +0,0 @@ -from typing import Dict, List, Optional, Final, Tuple, Pattern, Union, Callable -import ast - -# Commander selection configuration -# Format string for displaying duplicate cards in deck lists -DUPLICATE_CARD_FORMAT: Final[str] = '{card_name} x {count}' - -COMMANDER_CSV_PATH: Final[str] = 'csv_files/commander_cards.csv' -FUZZY_MATCH_THRESHOLD: Final[int] = 90 # Threshold for fuzzy name matching -MAX_FUZZY_CHOICES: Final[int] = 5 # Maximum number of fuzzy match choices -COMMANDER_CONVERTERS: Final[Dict[str, str]] = {'themeTags': ast.literal_eval, 'creatureTypes': ast.literal_eval} # CSV loading converters -# Commander-related constants -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]] = [] - -# 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] = 20 # 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 - -# Default fetch land count -FETCH_LAND_DEFAULT_COUNT: Final[int] = 3 # Default number of fetch lands to include - -# Basic land mappings -COLOR_TO_BASIC_LAND: Final[Dict[str, str]] = { - 'W': 'Plains', - 'U': 'Island', - 'B': 'Swamp', - 'R': 'Mountain', - 'G': 'Forest', - 'C': 'Wastes' -} - -# Dual land type mappings -DUAL_LAND_TYPE_MAP: Final[Dict[str, str]] = { - 'azorius': 'Plains Island', - 'dimir': 'Island Swamp', - 'rakdos': 'Swamp Mountain', - 'gruul': 'Mountain Forest', - 'selesnya': 'Forest Plains', - 'orzhov': 'Plains Swamp', - 'golgari': 'Swamp Forest', - 'simic': 'Forest Island', - 'izzet': 'Island Mountain', - 'boros': 'Mountain Plains' -} - -# Triple land type mappings -TRIPLE_LAND_TYPE_MAP: Final[Dict[str, str]] = { - 'bant': 'Forest Plains Island', - 'esper': 'Plains Island Swamp', - 'grixis': 'Island Swamp Mountain', - 'jund': 'Swamp Mountain Forest', - 'naya': 'Mountain Forest Plains', - 'mardu': 'Mountain Plains Swamp', - 'abzan': 'Plains Swamp Forest', - 'sultai': 'Swamp Forest Island', - 'temur': 'Forest Island Mountain', - 'jeska': 'Island Mountain Plains' -} - -# Default preference for including dual lands -DEFAULT_DUAL_LAND_ENABLED: Final[bool] = True - -# Default preference for including triple lands -DEFAULT_TRIPLE_LAND_ENABLED: Final[bool] = True - - -SNOW_COVERED_BASIC_LANDS: Final[Dict[str, str]] = { - 'W': 'Snow-Covered Plains', - 'U': 'Snow-Covered Island', - 'B': 'Snow-Covered Swamp', - 'G': 'Snow-Covered Forest' -} - -SNOW_BASIC_LAND_MAPPING: Final[Dict[str, str]] = { - 'W': 'Snow-Covered Plains', - 'U': 'Snow-Covered Island', - 'B': 'Snow-Covered Swamp', - 'R': 'Snow-Covered Mountain', - 'G': 'Snow-Covered Forest', - 'C': 'Wastes' # Note: No snow-covered version exists for Wastes -} - -# Generic fetch lands list -GENERIC_FETCH_LANDS: Final[List[str]] = [ - 'Evolving Wilds', - 'Terramorphic Expanse', - 'Shire Terrace', - 'Escape Tunnel', - 'Promising Vein', - 'Myriad Landscape', - 'Fabled Passage', - 'Terminal Moraine', - 'Prismatic Vista' -] - -# Kindred land constants -KINDRED_STAPLE_LANDS: Final[List[Dict[str, str]]] = [ - { - 'name': 'Path of Ancestry', - 'type': 'Land' - }, - { - 'name': 'Three Tree City', - 'type': 'Legendary Land' - }, - {'name': 'Cavern of Souls', 'type': 'Land'} -] - -# Color-specific fetch land mappings -COLOR_TO_FETCH_LANDS: Final[Dict[str, List[str]]] = { - 'W': [ - 'Flooded Strand', - 'Windswept Heath', - 'Marsh Flats', - 'Arid Mesa', - 'Brokers Hideout', - 'Obscura Storefront', - 'Cabaretti Courtyard' - ], - 'U': [ - 'Flooded Strand', - 'Polluted Delta', - 'Scalding Tarn', - 'Misty Rainforest', - 'Brokers Hideout', - 'Obscura Storefront', - 'Maestros Theater' - ], - 'B': [ - 'Polluted Delta', - 'Bloodstained Mire', - 'Marsh Flats', - 'Verdant Catacombs', - 'Obscura Storefront', - 'Maestros Theater', - 'Riveteers Overlook' - ], - 'R': [ - 'Bloodstained Mire', - 'Wooded Foothills', - 'Scalding Tarn', - 'Arid Mesa', - 'Maestros Theater', - 'Riveteers Overlook', - 'Cabaretti Courtyard' - ], - 'G': [ - 'Wooded Foothills', - 'Windswept Heath', - 'Verdant Catacombs', - 'Misty Rainforest', - 'Brokers Hideout', - 'Riveteers Overlook', - 'Cabaretti Courtyard' - ] -} - -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 - -# Staple land conditions mapping -STAPLE_LAND_CONDITIONS: Final[Dict[str, Callable[[List[str], List[str], int], bool]]] = { - 'Reliquary Tower': lambda commander_tags, colors, commander_power: True, # Always include - 'Ash Barrens': lambda commander_tags, colors, commander_power: 'Landfall' not in commander_tags, - 'Command Tower': lambda commander_tags, colors, commander_power: len(colors) > 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 -} - - -DEFAULT_CARD_ADVANTAGE_COUNT: Final[int] = 10 # Default number of card advantage pieces -DEFAULT_PROTECTION_COUNT: Final[int] = 8 # Default number of protection spells - -# 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: 20):', - '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 protection spells (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' -] - -# Card type constants -artifact_tokens: List[str] = ['Blood', 'Clue', 'Food', 'Gold', 'Incubator', - 'Junk','Map','Powerstone', 'Treasure'] - -banned_cards = [# in commander - 'Ancestral Recall', 'Balance', 'Biorhythm', 'Black Lotus', - 'Braids, Cabal Minion', 'Chaos Orb', 'Coalition Victory', - 'Channel', 'Dockside Extortionist', 'Emrakul, the Aeons Torn', - 'Erayo, Soratami Ascendant', 'Falling Star', 'Fastbond', - 'Flash', 'Gifts Ungiven', 'Golos, Tireless Pilgrim', - 'Griselbrand', 'Hullbreacher', 'Iona, Shield of Emeria', - 'Karakas', 'Jeweled Lotus', 'Leovold, Emissary of Trest', - 'Library of Alexandria', 'Limited Resources', 'Lutri, the Spellchaser', - 'Mana Crypt', 'Mox Emerald', 'Mox Jet', 'Mox Pearl', 'Mox Ruby', - 'Mox Sapphire', 'Nadu, Winged Wisdom', 'Panoptic Mirror', - 'Paradox Engine', 'Primeval Titan', 'Prophet of Kruphix', - 'Recurring Nightmare', 'Rofellos, Llanowar Emissary', 'Shahrazad', - 'Sundering Titan', 'Sway of the Stars', 'Sylvan Primordial', - 'Time Vault', 'Time Walk', 'Tinker', 'Tolarian Academy', - 'Trade Secrets', 'Upheaval', 'Yawgmoth\'s Bargain', - - # In constructed - 'Invoke Prejudice', 'Cleanse', 'Stone-Throwing Devils', 'Pradesh Gypsies', - 'Jihad', 'Imprison', 'Crusade' - ] - -BASIC_LANDS = ['Plains', 'Island', 'Swamp', 'Mountain', 'Forest'] - -# 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 + [land['name'] for land in KINDRED_STAPLE_LANDS] - -# Constants for lands matter functionality -LANDS_MATTER_PATTERNS: Dict[str, List[str]] = { - 'land_play': [ - 'play a land', - 'play an additional land', - 'play two additional lands', - 'play lands from', - 'put a land card', - 'put a basic land card' - ], - 'land_search': [ - 'search your library for a basic land card', - 'search your library for a land card', - 'search your library for up to two basic land', - 'search their library for a basic land card' - ], - 'land_state': [ - 'land enters', - 'land card is put into your graveyard', - 'number of lands you control', - 'one or more land cards', - 'sacrifice a land', - 'target land' - ] -} - -DOMAIN_PATTERNS = { - 'keyword': ['domain'], - 'text': ['basic land types among lands you control'] -} - -LANDFALL_PATTERNS = { - 'keyword': ['landfall'], - 'triggers': [ - 'whenever a land enters the battlefield under your control', - 'when a land enters the battlefield under your control' - ] -} - -LANDWALK_PATTERNS = { - 'basic': [ - 'plainswalker', - 'islandwalk', - 'swampwalk', - 'mountainwalk', - 'forestwalk' - ], - 'nonbasic': [ - 'nonbasic landwalk', - 'landwalk' - ] -} - -LAND_TYPES = [ - # Basic lands - 'Plains', 'Island', 'Swamp', 'Mountain', 'Forest', - # Special lands - 'Cave', 'Desert', 'Gate', 'Lair', 'Locus', 'Mine', - 'Power-Plant', 'Sphere', 'Tower', 'Urza\'s' -] - -LANDS_MATTER_SPECIFIC_CARDS = [ - 'Abundance', - 'Archdruid\'s Charm', - 'Archelos, Lagoon Mystic', - 'Catacylsmic Prospecting', - 'Coiling Oracle', - 'Disorienting Choice', - 'Eerie Ultimatum', - 'Gitrog Monster', - 'Mana Reflection', - 'Nahiri\'s Lithoforming', - 'Nine-fingers Keene', - 'Open the Way', - 'Realms Uncharted', - 'Reshape the Earth', - 'Scapeshift', - 'Yarok, the Desecrated', - 'Wonderscape Sage' -] - -# Constants for topdeck manipulation -TOPDECK_TEXT_PATTERNS = [ - 'from the top', - 'look at the top', - 'reveal the top', - 'scries', - 'surveils', - 'top of your library', - 'you scry', - 'you surveil' -] - -TOPDECK_KEYWORDS = [ - 'Miracle', - 'Scry', - 'Surveil' -] - -TOPDECK_SPECIFIC_CARDS = [ - 'Aminatou, the Fateshifter', - 'Brainstorm', - 'Counterbalance', - 'Delver of Secrets', - 'Jace, the Mind Sculptor', - 'Lantern of Insight', - 'Melek, Izzet Paragon', - 'Mystic Forge', - 'Sensei\'s Divining Top', - 'Soothsaying', - 'Temporal Mastery', - 'Vampiric Tutor' -] - -TOPDECK_EXCLUSION_PATTERNS = [ - 'from the top of target player\'s library', - 'from the top of their library', - 'look at the top card of target player\'s library', - 'reveal the top card of target player\'s library' -] - -# Constants for stax functionality - -# Constants for aristocrats functionality -ARISTOCRAT_TEXT_PATTERNS = [ - 'another creature dies', - 'creature dies', - 'creature dying', - 'creature you control dies', - 'creature you own dies', - 'dies this turn', - 'dies, create', - 'dies, draw', - 'dies, each opponent', - 'dies, exile', - 'dies, put', - 'dies, return', - 'dies, sacrifice', - 'dies, you', - 'has blitz', - 'have blitz', - 'permanents were sacrificed', - 'sacrifice a creature', - 'sacrifice another', - 'sacrifice another creature', - 'sacrifice a nontoken', - 'sacrifice a permanent', - 'sacrifice another nontoken', - 'sacrifice another permanent', - 'sacrifice another token', - 'sacrifices a creature', - 'sacrifices another', - 'sacrifices another creature', - 'sacrifices another nontoken', - 'sacrifices another permanent', - 'sacrifices another token', - 'sacrifices a nontoken', - 'sacrifices a permanent', - 'sacrifices a token', - 'when this creature dies', - 'whenever a food', - 'whenever you sacrifice' -] - -ARISTOCRAT_SPECIFIC_CARDS = [ - 'Ashnod, Flesh Mechanist', - 'Blood Artist', - 'Butcher of Malakir', - 'Chatterfang, Squirrel General', - 'Cruel Celebrant', - 'Dictate of Erebos', - 'Endrek Sahr, Master Breeder', - 'Gisa, Glorious Resurrector', - 'Grave Pact', - 'Grim Haruspex', - 'Judith, the Scourge Diva', - 'Korvold, Fae-Cursed King', - 'Mayhem Devil', - 'Midnight Reaper', - 'Mikaeus, the Unhallowed', - 'Pitiless Plunderer', - 'Poison-Tip Archer', - 'Savra, Queen of the Golgari', - 'Sheoldred, the Apocalypse', - 'Syr Konrad, the Grim', - 'Teysa Karlov', - 'Viscera Seer', - 'Yawgmoth, Thran Physician', - 'Zulaport Cutthroat' -] - -ARISTOCRAT_EXCLUSION_PATTERNS = [ - 'blocking enchanted', - 'blocking it', - 'blocked by', - 'end the turn', - 'from your graveyard', - 'from your hand', - 'from your library', - 'into your hand' -] - -STAX_TEXT_PATTERNS = [ - 'an opponent controls' - 'can\'t attack', - 'can\'t be cast', - 'can\'t be activated', - 'can\'t cast spells', - 'can\'t enter', - 'can\'t search', - 'can\'t untap', - 'don\'t untap', - 'don\'t cause abilities', - 'each other player\'s', - 'each player\'s upkeep', - 'opponent would search', - 'opponents cast cost', - 'opponents can\'t', - 'opponents control', - 'opponents control can\'t', - 'opponents control enter tapped', - 'spells cost {1} more', - 'spells cost {2} more', - 'spells cost {3} more', - 'spells cost {4} more', - 'spells cost {5} more', - 'that player doesn\'t', - 'unless that player pays', - 'you control your opponent', - 'you gain protection' -] - -STAX_SPECIFIC_CARDS = [ - 'Archon of Emeria', - 'Drannith Magistrate', - 'Ethersworn Canonist', - 'Grand Arbiter Augustin IV', - 'Hokori, Dust Drinker', - 'Kataki, War\'s Wage', - 'Lavinia, Azorius Renegade', - 'Leovold, Emissary of Trest', - 'Magus of the Moon', - 'Narset, Parter of Veils', - 'Opposition Agent', - 'Rule of Law', - 'Sanctum Prelate', - 'Thalia, Guardian of Thraben', - 'Winter Orb' -] - -STAX_EXCLUSION_PATTERNS = [ - 'blocking enchanted', - 'blocking it', - 'blocked by', - 'end the turn', - 'from your graveyard', - 'from your hand', - 'from your library', - 'into your hand' -] -# Constants for removal functionality -REMOVAL_TEXT_PATTERNS = [ - 'destroy target', - 'destroys target', - 'exile target', - 'exiles target', - 'sacrifices target', - 'return target.*to.*hand', - 'returns target.*to.*hand' -] - -REMOVAL_SPECIFIC_CARDS = ['from.*graveyard.*hand'] # type: list - -REMOVAL_EXCLUSION_PATTERNS = [] # type: list - -REMOVAL_KEYWORDS = [] # type: list - -# Constants for counterspell functionality -COUNTERSPELL_TEXT_PATTERNS = [ - 'control counters a', - 'counter target', - 'counter that spell', - 'counter all', - 'counter each', - 'counter the next', - 'counters a spell', - 'counters target', - 'return target spell', - 'exile target spell', - 'counter unless', - 'unless its controller pays' -] - -COUNTERSPELL_SPECIFIC_CARDS = [ - 'Arcane Denial', - 'Counterspell', - "Dovin's Veto", - 'Force of Will', - 'Mana Drain', - 'Mental Misstep', - 'Mindbreak Trap', - 'Mystic Confluence', - 'Pact of Negation', - 'Swan Song' -] - -COUNTERSPELL_EXCLUSION_PATTERNS = [ - 'counter on', - 'counter from', - 'remove a counter', - 'move a counter', - 'distribute counter', - 'proliferate' -] - -# Constants for theft functionality -THEFT_TEXT_PATTERNS = [ - 'cast a spell you don\'t own', - 'cast but don\'t own', - 'cost to cast this spell, sacrifice', - 'control but don\'t own', - 'exile top of target player\'s library', - 'exile top of each player\'s library', - 'gain control of', - 'target opponent\'s library', - 'that player\'s library', - 'you control enchanted creature' -] - -THEFT_SPECIFIC_CARDS = [ - 'Adarkar Valkyrie', - 'Captain N\'gathrod', - 'Hostage Taker', - 'Siphon Insight', - 'Thief of Sanity', - 'Xanathar, Guild Kingpin', - 'Zara, Renegade Recruiter' -] - -# Constants for big mana functionality -BIG_MANA_TEXT_PATTERNS = [ - 'add {w}{u}{b}{r}{g}', - 'card onto the battlefield', - 'control with power [3-5] or greater', - 'creature with power [3-5] or greater', - 'double the power', - 'from among them onto the battlefield', - 'from among them without paying', - 'hand onto the battlefield', - 'mana, add one mana', - 'mana, it produces twice', - 'mana, it produces three', - 'mana, its controller adds', - 'pay {w}{u}{b}{r}{g}', - 'spell with power 5 or greater', - 'value [5-7] or greater', - 'you may cast it without paying' -] - -BIG_MANA_SPECIFIC_CARDS = [ - 'Akroma\'s Memorial', - 'Apex Devastator', - 'Apex of Power', - 'Brass\'s Bounty', - 'Cabal Coffers', - 'Caged Sun', - 'Doubling Cube', - 'Forsaken Monument', - 'Guardian Project', - 'Mana Reflection', - 'Nyxbloom Ancient', - 'Omniscience', - 'One with the Multiverse', - 'Portal to Phyrexia', - 'Vorinclex, Voice of Hunger' -] - -BIG_MANA_KEYWORDS = [ - 'Cascade', - 'Convoke', - 'Discover', - 'Emerge', - 'Improvise', - 'Surge' -] - -# Constants for board wipe effects -BOARD_WIPE_TEXT_PATTERNS = { - 'mass_destruction': [ - 'destroy all', - 'destroy each', - 'destroy the rest', - 'destroys all', - 'destroys each', - 'destroys the rest' - ], - 'mass_exile': [ - 'exile all', - 'exile each', - 'exile the rest', - 'exiles all', - 'exiles each', - 'exiles the rest' - ], - 'mass_bounce': [ - 'return all', - 'return each', - 'put all creatures', - 'returns all', - 'returns each', - 'puts all creatures' - ], - 'mass_sacrifice': [ - 'sacrifice all', - 'sacrifice each', - 'sacrifice the rest', - 'sacrifices all', - 'sacrifices each', - 'sacrifices the rest' - ], - 'mass_damage': [ - 'deals damage to each', - 'deals damage to all', - 'deals X damage to each', - 'deals X damage to all', - 'deals that much damage to each', - 'deals that much damage to all' - ] -} - -BOARD_WIPE_SPECIFIC_CARDS = [ - 'Akroma\'s Vengeance', - 'All Is Dust', - 'Austere Command', - 'Blasphemous Act', - 'Cleansing Nova', - 'Cyclonic Rift', - 'Damnation', - 'Day of Judgment', - 'Decree of Pain', - 'Devastation Tide', - 'Evacuation', - 'Extinction Event', - 'Farewell', - 'Hour of Devastation', - 'In Garruk\'s Wake', - 'Living Death', - 'Living End', - 'Merciless Eviction', - 'Nevinyrral\'s Disk', - 'Oblivion Stone', - 'Planar Cleansing', - 'Ravnica at War', - 'Shatter the Sky', - 'Supreme Verdict', - 'Terminus', - 'Time Wipe', - 'Toxic Deluge', - 'Vanquish the Horde', - 'Wrath of God' -] - -BOARD_WIPE_EXCLUSION_PATTERNS = [ - 'blocking enchanted', - 'blocking it', - 'blocked by', - 'end the turn', - 'from your graveyard', - 'from your hand', - 'from your library', - 'into your hand', - 'target player\'s library', - 'that player\'s library' -] - -CARD_TYPES = ['Artifact','Creature', 'Enchantment', 'Instant', 'Land', 'Planeswalker', 'Sorcery', - 'Kindred', 'Dungeon', 'Battle'] - -# 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' -] - -# Default counts for each card type -CARD_TYPE_COUNT_DEFAULTS: Final[Dict[str, int]] = { - 'Artifact': 0, - 'Battle': 0, - 'Creature': 0, - 'Enchantment': 0, - 'Instant': 0, - 'Kindred': 0, - 'Land': 0, - 'Planeswalker': 0, - 'Sorcery': 0 -} - -# Mapping of card types to their corresponding theme tags -TYPE_TAG_MAPPING = { - 'Artifact': ['Artifacts Matter'], - 'Battle': ['Battles Matter'], - #'Creature': [], - 'Enchantment': ['Enchantments Matter'], - 'Equipment': ['Equipment', 'Voltron'], - 'Aura': ['Auras', 'Voltron'], - 'Instant': ['Spells Matter', 'Spellslinger'], - 'Land': ['Lands Matter'], - 'Planeswalker': ['Superfriends'], - 'Sorcery': ['Spells Matter', 'Spellslinger'] -} - -CSV_DIRECTORY = 'csv_files' -DECK_DIRECTORY = 'deck_files' - -# Color identity constants and mappings - -# 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']) -} - -# Color identity validation patterns -COLOR_IDENTITY_PATTERNS: Final[Dict[str, str]] = { - 'mono': r'^[WUBRG]$', - 'dual': r'^[WUBRG], [WUBRG]$', - 'tri': r'^[WUBRG], [WUBRG], [WUBRG]$', - 'four': r'^[WUBRG], [WUBRG], [WUBRG], [WUBRG]$', - 'five': r'^[WUBRG], [WUBRG], [WUBRG], [WUBRG], [WUBRG]$' -} - -COLORS = ['colorless', 'white', 'blue', 'black', 'red', 'green', - 'azorius', 'orzhov', 'selesnya', 'boros', 'dimir', - 'simic', 'izzet', 'golgari', 'rakdos', 'gruul', - 'bant', 'esper', 'grixis', 'jund', 'naya', - 'abzan', 'jeskai', 'mardu', 'sultai', 'temur', - 'dune', 'glint', 'ink', 'witch', 'yore', 'wubrg', - 'commander'] - -counter_types = [r'\+0/\+1', r'\+0/\+2', r'\+1/\+0', r'\+1/\+2', r'\+2/\+0', r'\+2/\+2', - '-0/-1', '-0/-2', '-1/-0', '-1/-2', '-2/-0', '-2/-2', - 'Acorn', 'Aegis', 'Age', 'Aim', 'Arrow', 'Arrowhead','Awakening', - 'Bait', 'Blaze', 'Blessing', 'Blight',' Blood', 'Bloddline', - 'Bloodstain', 'Book', 'Bounty', 'Brain', 'Bribery', 'Brick', - 'Burden', 'Cage', 'Carrion', 'Charge', 'Coin', 'Collection', - 'Component', 'Contested', 'Corruption', 'CRANK!', 'Credit', - 'Croak', 'Corpse', 'Crystal', 'Cube', 'Currency', 'Death', - 'Defense', 'Delay', 'Depletion', 'Descent', 'Despair', 'Devotion', - 'Divinity', 'Doom', 'Dream', 'Duty', 'Echo', 'Egg', 'Elixir', - 'Ember', 'Energy', 'Enlightened', 'Eon', 'Eruption', 'Everything', - 'Experience', 'Eyeball', 'Eyestalk', 'Fade', 'Fate', 'Feather', - 'Feeding', 'Fellowship', 'Fetch', 'Filibuster', 'Finality', 'Flame', - 'Flood', 'Foreshadow', 'Fungus', 'Fury', 'Fuse', 'Gem', 'Ghostform', - 'Glpyh', 'Gold', 'Growth', 'Hack', 'Harmony', 'Hatching', 'Hatchling', - 'Healing', 'Hit', 'Hope',' Hone', 'Hoofprint', 'Hour', 'Hourglass', - 'Hunger', 'Ice', 'Imposter', 'Incarnation', 'Incubation', 'Infection', - 'Influence', 'Ingenuity', 'Intel', 'Intervention', 'Invitation', - 'Isolation', 'Javelin', 'Judgment', 'Keyword', 'Ki', 'Kick', - 'Knickknack', 'Knowledge', 'Landmark', 'Level', 'Loot', 'Lore', - 'Loyalty', 'Luck', 'Magnet', 'Manabond', 'Manifestation', 'Mannequin', - 'Mask', 'Matrix', 'Memory', 'Midway', 'Mine', 'Mining', 'Mire', - 'Music', 'Muster', 'Necrodermis', 'Nest', 'Net', 'Night', 'Oil', - 'Omen', 'Ore', 'Page', 'Pain', 'Palliation', 'Paralyzing', 'Pause', - 'Petal', 'Petrification', 'Phyresis', 'Phylatery', 'Pin', 'Plague', - 'Plot', 'Point', 'Poison', 'Polyp', 'Possession', 'Pressure', 'Prey', - 'Pupa', 'Quest', 'Rad', 'Rejection', 'Reprieve', 'Rev', 'Revival', - 'Ribbon', 'Ritual', 'Rope', 'Rust', 'Scream', 'Scroll', 'Shell', - 'Shield', 'Silver', 'Shred', 'Sleep', 'Sleight', 'Slime', 'Slumber', - 'Soot', 'Soul', 'Spark', 'Spite', 'Spore', 'Stash', 'Storage', - 'Story', 'Strife', 'Study', 'Stun', 'Supply', 'Suspect', 'Takeover', - 'Task', 'Ticket', 'Tide', 'Time', 'Tower', 'Training', 'Trap', - 'Treasure', 'Unity', 'Unlock', 'Valor', 'Velocity', 'Verse', - 'Vitality', 'Void', 'Volatile', 'Vortex', 'Vow', 'Voyage', 'Wage', - 'Winch', 'Wind', 'Wish'] - -creature_types = ['Advisor', 'Aetherborn', 'Alien', 'Ally', 'Angel', 'Antelope', 'Ape', 'Archer', 'Archon', 'Armadillo', - 'Army', 'Artificer', 'Assassin', 'Assembly-Worker', 'Astartes', 'Atog', 'Aurochs', 'Automaton', - 'Avatar', 'Azra', 'Badger', 'Balloon', 'Barbarian', 'Bard', 'Basilisk', 'Bat', 'Bear', 'Beast', 'Beaver', - 'Beeble', 'Beholder', 'Berserker', 'Bird', 'Blinkmoth', 'Boar', 'Brainiac', 'Bringer', 'Brushwagg', - 'C\'tan', 'Camarid', 'Camel', 'Capybara', 'Caribou', 'Carrier', 'Cat', 'Centaur', 'Chicken', 'Child', - 'Chimera', 'Citizen', 'Cleric', 'Clown', 'Cockatrice', 'Construct', 'Coward', 'Coyote', 'Crab', 'Crocodile', - 'Custodes', 'Cyberman', 'Cyclops', 'Dalek', 'Dauthi', 'Demigod', 'Demon', 'Deserter', 'Detective', 'Devil', - 'Dinosaur', 'Djinn', 'Doctor', 'Dog', 'Dragon', 'Drake', 'Dreadnought', 'Drone', 'Druid', 'Dryad', 'Dwarf', - 'Efreet', 'Egg', 'Elder', 'Eldrazi', 'Elemental', 'Elephant', 'Elf', 'Elk', 'Employee', 'Eye', 'Faerie', - 'Ferret', 'Fish', 'Flagbearer', 'Fox', 'Fractal', 'Frog', 'Fungus', 'Gamer', 'Gargoyle', 'Germ', 'Giant', - 'Gith', 'Glimmer', 'Gnoll', 'Gnome', 'Goat', 'Goblin', 'God', 'Golem', 'Gorgon', 'Graveborn', 'Gremlin', - 'Griffin', 'Guest', 'Hag', 'Halfling', 'Hamster', 'Harpy', 'Head', 'Hellion', 'Hero', 'Hippo', 'Hippogriff', - 'Homarid', 'Homunculus', 'Hornet', 'Horror', 'Horse', 'Human', 'Hydra', 'Hyena', 'Illusion', 'Imp', - 'Incarnation', 'Inkling', 'Inquisitor', 'Insect', 'Jackal', 'Jellyfish', 'Juggernaut', 'Kavu', 'Kirin', - 'Kithkin', 'Knight', 'Kobold', 'Kor', 'Kraken', 'Lamia', 'Lammasu', 'Leech', 'Leviathan', 'Lhurgoyf', - 'Licid', 'Lizard', 'Manticore', 'Masticore', 'Mercenary', 'Merfolk', 'Metathran', 'Minion', 'Minotaur', - 'Mite', 'Mole', 'Monger', 'Mongoose', 'Monk', 'Monkey', 'Moonfolk', 'Mount', 'Mouse', 'Mutant', 'Myr', - 'Mystic', 'Naga', 'Nautilus', 'Necron', 'Nephilim', 'Nightmare', 'Nightstalker', 'Ninja', 'Noble', 'Noggle', - 'Nomad', 'Nymph', 'Octopus', 'Ogre', 'Ooze', 'Orb', 'Orc', 'Orgg', 'Otter', 'Ouphe', 'Ox', 'Oyster', 'Pangolin', - 'Peasant', 'Pegasus', 'Pentavite', 'Performer', 'Pest', 'Phelddagrif', 'Phoenix', 'Phyrexian', 'Pilot', - 'Pincher', 'Pirate', 'Plant', 'Porcupine', 'Possum', 'Praetor', 'Primarch', 'Prism', 'Processor', 'Rabbit', - 'Raccoon', 'Ranger', 'Rat', 'Rebel', 'Reflection', 'Reveler', 'Rhino', 'Rigger', 'Robot', 'Rogue', 'Rukh', - 'Sable', 'Salamander', 'Samurai', 'Sand', 'Saproling', 'Satyr', 'Scarecrow', 'Scientist', 'Scion', 'Scorpion', - 'Scout', 'Sculpture', 'Serf', 'Serpent', 'Servo', 'Shade', 'Shaman', 'Shapeshifter', 'Shark', 'Sheep', 'Siren', - 'Skeleton', 'Skunk', 'Slith', 'Sliver', 'Sloth', 'Slug', 'Snail', 'Snake', 'Soldier', 'Soltari', 'Spawn', - 'Specter', 'Spellshaper', 'Sphinx', 'Spider', 'Spike', 'Spirit', 'Splinter', 'Sponge', 'Spy', 'Squid', - 'Squirrel', 'Starfish', 'Surrakar', 'Survivor', 'Synth', 'Teddy', 'Tentacle', 'Tetravite', 'Thalakos', - 'Thopter', 'Thrull', 'Tiefling', 'Time Lord', 'Toy', 'Treefolk', 'Trilobite', 'Triskelavite', 'Troll', - 'Turtle', 'Tyranid', 'Unicorn', 'Urzan', 'Vampire', 'Varmint', 'Vedalken', 'Volver', 'Wall', 'Walrus', - 'Warlock', 'Warrior', 'Wasp', 'Weasel', 'Weird', 'Werewolf', 'Whale', 'Wizard', 'Wolf', 'Wolverine', 'Wombat', - 'Worm', 'Wraith', 'Wurm', 'Yeti', 'Zombie', 'Zubera'] - -enchantment_tokens = ['Cursed Role', 'Monster Role', 'Royal Role', 'Sorcerer Role', - 'Virtuous Role', 'Wicked Role', 'Young Hero Role', 'Shard'] - -multiple_copy_cards = ['Dragon\'s Approach', 'Hare Apparent', 'Nazgûl', 'Persistent Petitioners', - 'Rat Colony', 'Relentless Rats', 'Seven Dwarves', 'Shadowborn Apostle', - 'Slime Against Humanity', 'Templar Knight'] - -non_creature_types = ['Legendary', 'Creature', 'Enchantment', 'Artifact', - 'Battle', 'Sorcery', 'Instant', 'Land', '-', '—', - 'Blood', 'Clue', 'Food', 'Gold', 'Incubator', - 'Junk', 'Map', 'Powerstone', 'Treasure', - 'Equipment', 'Fortification', 'vehicle', - 'Bobblehead', 'Attraction', 'Contraption', - 'Siege', - 'Aura', 'Background', 'Saga', 'Role', 'Shard', - 'Cartouche', 'Case', 'Class', 'Curse', 'Rune', - 'Shrine', - 'Plains', 'Island', 'Swamp', 'Forest', 'Mountain', - 'Cave', 'Desert', 'Gate', 'Lair', 'Locus', 'Mine', - 'Power-Plant', 'Sphere', 'Tower', 'Urza\'s'] - -num_to_search = ['a', 'an', 'one', '1', 'two', '2', 'three', '3', 'four','4', 'five', '5', - 'six', '6', 'seven', '7', 'eight', '8', 'nine', '9', 'ten', '10', - 'x','one or more'] - -theme_tags = ['+1/+1 counter', 'one or more counters', 'token', 'gain life', 'one or more creature tokens', - 'creature token', 'treasure', 'create token', 'draw a card', 'flash', 'choose a creature type', - 'play land', 'artifact you control enters', 'enchantment you control enters', 'poison counter', - 'from graveyard', 'mana value', 'from exile', 'mana of any color', 'attacks', 'total power', - 'greater than starting life', 'lose life', 'whenever you sacrifice', 'creature dying', - 'creature enters', 'creature leaves', 'creature dies', 'put into graveyard', 'sacrifice', - 'sacrifice creature', 'sacrifice artifact', 'sacrifice another creature', '-1/-1 counter', - 'control get +1/+1', 'control dies', 'experience counter', 'triggered ability', 'token', - 'commit a crime'] - -targetted_removal_tags = ['exile target', 'destroy target', 'return target', 'shuffles target', 'you control', - 'deals damage to target', 'loses all abilities'] - -triggers = ['when', 'whenever', 'at'] - -# Constants for draw-related functionality -DRAW_RELATED_TAGS = [ - 'Card Draw', # General card draw effects - 'Conditional Draw', # Draw effects with conditions/triggers - 'Cycling', # Cycling and similar discard-to-draw effects - 'Life to Draw', # Draw effects that require paying life - 'Loot', # Draw + discard effects - 'Replacement Draw', # Effects that modify or replace draws - 'Sacrifice to Draw', # Draw effects requiring sacrificing permanents - 'Unconditional Draw' # Pure card draw without conditions -] - -# Text patterns that exclude cards from being tagged as unconditional draw -DRAW_EXCLUSION_PATTERNS = [ - 'annihilator', # Eldrazi mechanic that can match 'draw' patterns - 'ravenous', # Keyword that can match 'draw' patterns -] - -# Constants for DataFrame validation and processing -REQUIRED_COLUMNS: List[str] = [ - 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', - 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', - 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' -] - -# 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.3, - 'hidden': 0.0 -} - -WEIGHT_ADJUSTMENT_FACTORS: Final[Dict[str, float]] = { - 'kindred_primary': 1.5, # 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', 'Protection', 'Ramp', 'Reanimate', - 'Removal', 'Sacrifice Matters', 'Spellslinger', 'Stax', 'Super Friends', - 'Theft', 'Token Creation', 'Tokens Matter', 'Voltron', 'X Spells' -] - -COLUMN_ORDER = [ - 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', - 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', - 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' -] - -PRETAG_COLUMN_ORDER: List[str] = [ - 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', - 'manaCost', 'manaValue', 'type', 'text', 'power', 'toughness', - 'keywords', 'layout', 'side' -] - -TAGGED_COLUMN_ORDER: List[str] = [ - 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', - 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', - 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' -] - -EXCLUDED_CARD_TYPES: List[str] = ['Plane —', 'Conspiracy', 'Vanguard', 'Scheme', - 'Phenomenon', 'Stickers', 'Attraction', 'Hero', - 'Contraption'] -# Constants for type detection and processing -OUTLAW_TYPES = ['Assassin', 'Mercenary', 'Pirate', 'Rogue', 'Warlock'] -TYPE_DETECTION_BATCH_SIZE = 1000 - -# Aura-related constants -AURA_SPECIFIC_CARDS = [ - 'Ardenn, Intrepid Archaeologist', # Aura movement - 'Calix, Guided By Fate', # Create duplicate Auras - 'Gilwain, Casting Director', # Creates role tokens - 'Ivy, Gleeful Spellthief', # Copies spells that have single target - 'Killian, Ink Duelist', # Targetted spell cost reduction -] - -# Equipment-related constants -EQUIPMENT_EXCLUSIONS = [ - 'Bruenor Battlehammer', # Equipment cost reduction - 'Nazahn, Revered Bladesmith', # Equipment tutor - 'Stonehewer Giant', # Equipment tutor -] - -EQUIPMENT_SPECIFIC_CARDS = [ - 'Ardenn, Intrepid Archaeologist', # Equipment movement - 'Armory Automaton', # Mass equip ability - 'Brass Squire', # Free equip ability - 'Danitha Capashen, Paragon', # Equipment cost reduction - 'Halvar, God of Battle', # Equipment movement - 'Kemba, Kha Regent', # Equipment payoff - 'Kosei, Penitent Warlord', # Wants to be eequipped - 'Puresteel Paladin', # Equipment draw engine - 'Reyav, Master Smith', # Equipment combat boost - 'Sram, Senior Edificer', # Equipment card draw - 'Valduk, Keeper of the Flame' # Equipment token creation -] - -EQUIPMENT_RELATED_TAGS = [ - 'Equipment', # Base equipment tag - 'Equipment Matters', # Cards that care about equipment - 'Voltron', # Commander-focused equipment strategy - 'Artifacts Matter', # Equipment are artifacts - 'Warriors Matter', # Common equipment tribal synergy - 'Knights Matter' # Common equipment tribal synergy -] - -EQUIPMENT_TEXT_PATTERNS = [ - 'attach', # Equipment attachment - 'equip', # Equipment keyword - 'equipped', # Equipment state - 'equipment', # Equipment type - 'unattach', # Equipment removal - 'unequip', # Equipment removal -] - - -# Constants for Voltron strategy -VOLTRON_COMMANDER_CARDS = [ - 'Akiri, Line-Slinger', - 'Ardenn, Intrepid Archaeologist', - 'Bruna, Light of Alabaster', - 'Danitha Capashen, Paragon', - 'Greven, Predator Captain', - 'Halvar, God of Battle', - 'Kaldra Compleat', - 'Kemba, Kha Regent', - 'Light-Paws, Emperor\'s Voice', - 'Nahiri, the Lithomancer', - 'Rafiq of the Many', - 'Reyav, Master Smith', - 'Rograkh, Son of Rohgahh', - 'Sram, Senior Edificer', - 'Syr Gwyn, Hero of Ashvale', - 'Tiana, Ship\'s Caretaker', - 'Uril, the Miststalker', - 'Valduk, Keeper of the Flame', - 'Wyleth, Soul of Steel' -] - -VOLTRON_PATTERNS = [ - 'attach', - 'aura you control', - 'enchant creature', - 'enchanted creature', - 'equipped creature', - 'equipment you control', - 'fortify', - 'living weapon', - 'reconfigure' -] - -# Constants for price checking functionality -PRICE_CHECK_CONFIG: Dict[str, float] = { - # Maximum number of retry attempts for price checking requests - 'max_retries': 3, - - # Timeout in seconds for price checking requests - 'timeout': 0.1, - - # Maximum size of the price check cache - 'cache_size': 128, - - # Price tolerance factor (e.g., 1.1 means accept prices within 10% difference) - 'price_tolerance': 1.1 -} - -# 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 - -# DataFrame validation configuration -MIN_EDHREC_RANK: int = 0 -MAX_EDHREC_RANK: int = 100000 -MIN_MANA_VALUE: int = 0 -MAX_MANA_VALUE: int = 20 - -# 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 category validation rules -CREATURE_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float, bool]]]] = { - 'power': {'type': ('str', 'int', 'float', 'object'), 'required': True}, - 'toughness': {'type': ('str', 'int', 'float', 'object'), 'required': True}, - 'creatureTypes': {'type': ('list', 'object'), 'required': True} -} - -SPELL_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float, bool]]]] = { - 'manaCost': {'type': 'str', 'required': True}, - 'text': {'type': 'str', 'required': True} -} - -LAND_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float, bool]]]] = { - 'type': {'type': ('str', 'object'), 'required': True}, - 'text': {'type': ('str', 'object'), 'required': False} -} - -# Column mapping configurations -DATAFRAME_COLUMN_MAPS: Final[Dict[str, Dict[str, str]]] = { - 'creature': { - 'name': 'Card Name', - 'type': 'Card Type', - 'manaCost': 'Mana Cost', - 'manaValue': 'Mana Value', - 'power': 'Power', - 'toughness': 'Toughness' - }, - 'spell': { - 'name': 'Card Name', - 'type': 'Card Type', - 'manaCost': 'Mana Cost', - 'manaValue': 'Mana Value' - }, - 'land': { - 'name': 'Card Name', - 'type': 'Card Type' - } -} - -# Required DataFrame columns -DATAFRAME_REQUIRED_COLUMNS: Final[List[str]] = [ - 'name', 'type', 'colorIdentity', 'manaValue', 'text', - 'edhrecRank', 'themeTags', 'keywords' -] - -# 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, Union[str, int, float]]]] = { - '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*+-]+$'} -} - -# Required columns for CSV validation -CSV_REQUIRED_COLUMNS: Final[List[str]] = [ - 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', - 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', - 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' -] -# Constants for setup and CSV processing -MTGJSON_API_URL = 'https://mtgjson.com/api/v5/csv/cards.csv' - -LEGENDARY_OPTIONS = [ - 'Legendary Creature', - 'Legendary Artifact', - 'Legendary Artifact Creature', - 'Legendary Enchantment Creature', - 'Legendary Planeswalker' -] - -NON_LEGAL_SETS = [ - 'PHTR', 'PH17', 'PH18', 'PH19', 'PH20', 'PH21', - 'UGL', 'UND', 'UNH', 'UST' -] - -CARD_TYPES_TO_EXCLUDE = [ - 'Plane —', - 'Conspiracy', - 'Vanguard', - 'Scheme', - 'Phenomenon', - 'Stickers', - 'Attraction', - 'Hero', - 'Contraption' -] - -# Columns to keep when processing CSV files -CSV_PROCESSING_COLUMNS = [ - 'name', # Card name - 'faceName', # Name of specific face for multi-faced cards - 'edhrecRank', # Card's rank on EDHREC - 'colorIdentity', # Color identity for Commander format - 'colors', # Actual colors in card's mana cost - 'manaCost', # Mana cost string - 'manaValue', # Converted mana cost - 'type', # Card type line - 'layout', # Card layout (normal, split, etc) - 'text', # Card text/rules - 'power', # Power (for creatures) - 'toughness', # Toughness (for creatures) - 'keywords', # Card's keywords - 'side' # Side identifier for multi-faced cards -] - -SETUP_COLORS = ['colorless', 'white', 'blue', 'black', 'green', 'red', - 'azorius', 'orzhov', 'selesnya', 'boros', 'dimir', - 'simic', 'izzet', 'golgari', 'rakdos', 'gruul', - 'bant', 'esper', 'grixis', 'jund', 'naya', - 'abzan', 'jeskai', 'mardu', 'sultai', 'temur', - 'dune', 'glint', 'ink', 'witch', 'yore', 'wubrg'] - -COLOR_ABRV = ['Colorless', 'W', 'U', 'B', 'G', 'R', - 'U, W', 'B, W', 'G, W', 'R, W', 'B, U', - 'G, U', 'R, U', 'B, G', 'B, R', 'G, R', - 'G, U, W', 'B, U, W', 'B, R, U', 'B, G, R', 'G, R, W', - 'B, G, W', 'R, U, W', 'B, R, W', 'B, G, U', 'G, R, U', - 'B, G, R, W', 'B, G, R, U', 'G, R, U, W', 'B, G, U, W', - 'B, R, U, W', 'B, G, R, U, W'] - -# Configuration for handling null/NA values in DataFrame columns -FILL_NA_COLUMNS: Dict[str, Optional[str]] = { - 'colorIdentity': 'Colorless', # Default color identity for cards without one - 'faceName': None # Use card's name column value when face name is not available -} -# Configuration for DataFrame sorting operations -SORT_CONFIG = { - 'columns': ['name', 'side'], # Columns to sort by - 'case_sensitive': False # Ignore case when sorting -} - -# Configuration for DataFrame filtering operations -FILTER_CONFIG: Dict[str, Dict[str, List[str]]] = { - 'layout': { - 'exclude': ['reversible_card'] - }, - 'availability': { - 'require': ['paper'] - }, - 'promoTypes': { - 'exclude': ['playtest'] - }, - 'securityStamp': { - 'exclude': ['Heart', 'Acorn'] - } -} \ No newline at end of file