mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-22 04:50:46 +02:00
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:
parent
3a5beebfe2
commit
dbbc8bc66e
20 changed files with 1525 additions and 1737 deletions
7
code/deck_builder/__init__.py
Normal file
7
code/deck_builder/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from .builder import DeckBuilder
|
||||
from .builder_utils import *
|
||||
from .builder_constants import *
|
||||
|
||||
__all__ = [
|
||||
'DeckBuilder',
|
||||
]
|
|
@ -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)
|
437
code/deck_builder/builder_constants.py
Normal file
437
code/deck_builder/builder_constants.py
Normal 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'
|
||||
]
|
|
@ -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[
|
8
code/file_setup/__init__.py
Normal file
8
code/file_setup/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
"""Initialize the file_setup package."""
|
||||
|
||||
from .setup import setup, regenerate_csv_by_color
|
||||
|
||||
__all__ = [
|
||||
'setup',
|
||||
'regenerate_csv_by_color'
|
||||
]
|
|
@ -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')
|
118
code/file_setup/setup_constants.py
Normal file
118
code/file_setup/setup_constants.py
Normal 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'
|
||||
]
|
|
@ -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):
|
|
@ -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
29
code/logging_util.py
Normal 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))
|
|
@ -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)
|
|
@ -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
40
code/settings.py
Normal 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
4
code/tagging/__init__.py
Normal 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.
|
||||
"""
|
718
code/tagging/tag_constants.py
Normal file
718
code/tagging/tag_constants.py
Normal 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'
|
||||
]
|
|
@ -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:
|
|
@ -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')
|
1376
settings.py
1376
settings.py
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue