Moved the builder, tagger, and setup modules into their own folders, along with constants to help provide better clarity and readability. Additionally added a missing call for the tag_for_artifcact_triggers() function

This commit is contained in:
mwisnowski 2025-01-28 10:19:44 -08:00
parent 3a5beebfe2
commit dbbc8bc66e
20 changed files with 1525 additions and 1737 deletions

View file

@ -0,0 +1,7 @@
from .builder import DeckBuilder
from .builder_utils import *
from .builder_constants import *
__all__ = [
'DeckBuilder',
]

View file

@ -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)

View file

@ -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'
]

View file

@ -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[

View file

@ -0,0 +1,8 @@
"""Initialize the file_setup package."""
from .setup import setup, regenerate_csv_by_color
__all__ = [
'setup',
'regenerate_csv_by_color'
]

View file

@ -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')

View file

@ -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'
]

View file

@ -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):

View file

@ -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.

29
code/logging_util.py Normal file
View file

@ -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))

View file

@ -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)

View file

@ -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.

40
code/settings.py Normal file
View file

@ -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']

4
code/tagging/__init__.py Normal file
View file

@ -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.
"""

View file

@ -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'
]

View file

@ -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:

View file

@ -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,11 +1113,15 @@ 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')
print('\n==========\n')
@ -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')

File diff suppressed because it is too large Load diff