mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-21 20:40:47 +02:00
6466 lines
221 KiB
Python
6466 lines
221 KiB
Python
from __future__ import annotations
|
|
|
|
# Standard library imports
|
|
import logging
|
|
import os
|
|
import re
|
|
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.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.StreamHandler(),
|
|
logging.FileHandler('logs/tagger.log', mode='w', encoding='utf-8')
|
|
]
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
### Setup
|
|
## Load the dataframe
|
|
def load_dataframe(color: str) -> None:
|
|
"""
|
|
Load and validate the card dataframe for a given color.
|
|
|
|
Args:
|
|
color (str): The color of cards to load ('white', 'blue', etc)
|
|
|
|
Raises:
|
|
FileNotFoundError: If CSV file doesn't exist and can't be regenerated
|
|
ValueError: If required columns are missing
|
|
"""
|
|
try:
|
|
filepath = f'{CSV_DIRECTORY}/{color}_cards.csv'
|
|
|
|
# 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)
|
|
if not os.path.exists(filepath):
|
|
raise FileNotFoundError(f"Failed to generate {filepath}")
|
|
|
|
# Load initial dataframe for validation
|
|
check_df = pd.read_csv(filepath)
|
|
|
|
# Validate required columns
|
|
required_columns = ['creatureTypes', 'themeTags']
|
|
missing_columns = [col for col in required_columns if col not in check_df.columns]
|
|
|
|
# Handle missing columns
|
|
if missing_columns:
|
|
logger.warning(f"Missing columns: {missing_columns}")
|
|
if 'creatureTypes' not in check_df.columns:
|
|
kindred_tagging(check_df, color)
|
|
if 'themeTags' not in check_df.columns:
|
|
create_theme_tags(check_df, color)
|
|
|
|
# Verify columns were added successfully
|
|
check_df = pd.read_csv(filepath)
|
|
still_missing = [col for col in required_columns if col not in check_df.columns]
|
|
if still_missing:
|
|
raise ValueError(f"Failed to add required columns: {still_missing}")
|
|
|
|
# Load final dataframe with proper converters
|
|
df = pd.read_csv(filepath, converters={'themeTags': pd.eval, 'creatureTypes': pd.eval})
|
|
|
|
# Process the dataframe
|
|
tag_by_color(df, color)
|
|
|
|
except FileNotFoundError as e:
|
|
logger.error(f'Error: {e}')
|
|
raise
|
|
except pd.errors.ParserError as e:
|
|
logger.error(f'Error parsing the CSV file: {e}')
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f'An unexpected error occurred: {e}')
|
|
raise
|
|
|
|
## Tag cards on a color-by-color basis
|
|
def tag_by_color(df: pd.DataFrame, color: str) -> None:
|
|
|
|
#load_dataframe()
|
|
#answer = input('Would you like to regenerate the CSV file?\n')
|
|
#if answer.lower() in ['yes', 'y']:
|
|
# regenerate_csv_by_color(color)
|
|
# kindred_tagging(df, color)
|
|
# create_theme_tags(df, color)
|
|
#else:
|
|
# pass
|
|
kindred_tagging(df, color)
|
|
print('\n====================\n')
|
|
create_theme_tags(df, color)
|
|
print('\n====================\n')
|
|
|
|
# Go through each type of tagging
|
|
add_creatures_to_tags(df, color)
|
|
print('\n====================\n')
|
|
tag_for_card_types(df, color)
|
|
print('\n====================\n')
|
|
tag_for_keywords(df, color)
|
|
print('\n====================\n')
|
|
|
|
## Tag for various effects
|
|
tag_for_cost_reduction(df, color)
|
|
print('\n====================\n')
|
|
tag_for_card_draw(df, color)
|
|
print('\n====================\n')
|
|
tag_for_artifacts(df, color)
|
|
print('\n====================\n')
|
|
tag_for_enchantments(df, color)
|
|
print('\n====================\n')
|
|
tag_for_exile_matters(df, color)
|
|
print('\n====================\n')
|
|
tag_for_tokens(df, color)
|
|
print('\n====================\n')
|
|
tag_for_life_matters(df, color)
|
|
print('\n====================\n')
|
|
tag_for_counters(df, color)
|
|
print('\n====================\n')
|
|
tag_for_voltron(df, color)
|
|
print('\n====================\n')
|
|
tag_for_lands_matter(df, color)
|
|
print('\n====================\n')
|
|
tag_for_spellslinger(df, color)
|
|
print('\n====================\n')
|
|
tag_for_ramp(df, color)
|
|
print('\n====================\n')
|
|
tag_for_themes(df, color)
|
|
print('\n====================\n')
|
|
tag_for_interaction(df, color)
|
|
print('\n====================\n')
|
|
|
|
# Lastly, sort all theme tags for easier reading
|
|
sort_theme_tags(df, color)
|
|
df.to_csv(f'{CSV_DIRECTORY}/{color}_cards.csv', index=False)
|
|
#print(df)
|
|
print('\n====================\n')
|
|
logger.info(f'Tags are done being set on {color}_cards.csv')
|
|
#keyboard.wait('esc')
|
|
|
|
## Determine any non-creature cards that have creature types mentioned
|
|
def kindred_tagging(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with creature types and related types.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Setting creature type tags on {color}_cards.csv')
|
|
|
|
try:
|
|
# Initialize creatureTypes column vectorized
|
|
df['creatureTypes'] = pd.Series([[] for _ in range(len(df))])
|
|
|
|
# Detect creature types using mask
|
|
creature_mask = tag_utils.create_type_mask(df, 'Creature')
|
|
if creature_mask.any():
|
|
creature_rows = df[creature_mask]
|
|
for idx, row in creature_rows.iterrows():
|
|
types = tag_utils.extract_creature_types(
|
|
row['type'],
|
|
settings.creature_types,
|
|
settings.non_creature_types
|
|
)
|
|
if types:
|
|
df.at[idx, 'creatureTypes'] = types
|
|
|
|
creature_time = pd.Timestamp.now()
|
|
logger.info(f'Creature type detection completed in {(creature_time - start_time).total_seconds():.2f}s')
|
|
print('\n==========\n')
|
|
|
|
logger.info(f'Setting Outlaw creature type tags on {color}_cards.csv')
|
|
# Process outlaw types
|
|
outlaws = settings.OUTLAW_TYPES
|
|
df['creatureTypes'] = df.apply(
|
|
lambda row: tag_utils.add_outlaw_type(row['creatureTypes'], outlaws)
|
|
if isinstance(row['creatureTypes'], list) else row['creatureTypes'],
|
|
axis=1
|
|
)
|
|
|
|
outlaw_time = pd.Timestamp.now()
|
|
logger.info(f'Outlaw type processing completed in {(outlaw_time - creature_time).total_seconds():.2f}s')
|
|
|
|
# Find creature types in text
|
|
logger.info('Checking for creature types in card text')
|
|
# Check for creature types in text (i.e. how 'Voja, Jaws of the Conclave' cares about Elves)
|
|
logger.info(f'Checking for and setting creature types found in the text of cards in {color}_cards.csv')
|
|
ignore_list = [
|
|
'Elite Inquisitor', 'Breaker of Armies',
|
|
'Cleopatra, Exiled Pharaoh', 'Nath\'s Buffoon'
|
|
]
|
|
|
|
for idx, row in df.iterrows():
|
|
if row['name'] not in ignore_list:
|
|
text_types = tag_utils.find_types_in_text(
|
|
row['text'],
|
|
row['name'],
|
|
settings.creature_types
|
|
)
|
|
if text_types:
|
|
current_types = row['creatureTypes']
|
|
if isinstance(current_types, list):
|
|
df.at[idx, 'creatureTypes'] = sorted(
|
|
list(set(current_types + text_types))
|
|
)
|
|
|
|
text_time = pd.Timestamp.now()
|
|
logger.info(f'Text-based type detection completed in {(text_time - outlaw_time).total_seconds():.2f}s')
|
|
|
|
# Save results
|
|
try:
|
|
columns_to_keep = [
|
|
'name', 'faceName', 'edhrecRank', 'colorIdentity',
|
|
'colors', 'manaCost', 'manaValue', 'type',
|
|
'creatureTypes', 'text', 'power', 'toughness',
|
|
'keywords', 'layout', 'side'
|
|
]
|
|
df = df[columns_to_keep]
|
|
df.to_csv(f'{settings.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')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error saving results: {e}')
|
|
|
|
# Overwrite file with creature type tags
|
|
except Exception as e:
|
|
logger.error(f'Error in kindred_tagging: {e}')
|
|
raise
|
|
|
|
def create_theme_tags(df: pd.DataFrame, color: str) -> None:
|
|
"""Initialize and configure theme tags for a card DataFrame.
|
|
|
|
This function initializes the themeTags column, validates the DataFrame structure,
|
|
and reorganizes columns in an efficient manner. It uses vectorized operations
|
|
for better performance.
|
|
|
|
Args:
|
|
df: DataFrame containing card data to process
|
|
color: Color identifier for logging purposes (e.g. 'white', 'blue')
|
|
|
|
Returns:
|
|
The processed DataFrame with initialized theme tags and reorganized columns
|
|
|
|
Raises:
|
|
ValueError: If required columns are missing or color is invalid
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info('Initializing theme tags for %s cards', color)
|
|
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
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:
|
|
raise ValueError(f"Invalid color: {color}")
|
|
|
|
try:
|
|
# Initialize themeTags column using vectorized operation
|
|
df['themeTags'] = pd.Series([[] for _ in range(len(df))], index=df.index)
|
|
|
|
# Define expected columns
|
|
required_columns = {
|
|
'name', 'text', 'type', 'keywords',
|
|
'creatureTypes', 'power', 'toughness'
|
|
}
|
|
|
|
# Validate required columns
|
|
missing = required_columns - set(df.columns)
|
|
if missing:
|
|
raise ValueError(f"Missing required columns: {missing}")
|
|
|
|
# Define column order
|
|
columns_to_keep = settings.REQUIRED_COLUMNS
|
|
|
|
# Reorder columns efficiently
|
|
available_cols = [col for col in columns_to_keep if col in df.columns]
|
|
df = df.reindex(columns=available_cols)
|
|
|
|
# Save results
|
|
try:
|
|
df.to_csv(f'{settings.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')
|
|
|
|
# Log performance metrics
|
|
end_time = pd.Timestamp.now()
|
|
duration = (end_time - start_time).total_seconds()
|
|
logger.info('Theme tags initialized in %.2f seconds', duration)
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error saving results: {e}')
|
|
|
|
except Exception as e:
|
|
logger.error('Error initializing theme tags: %s', str(e))
|
|
raise
|
|
|
|
def tag_for_card_types(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards based on their types using vectorized operations.
|
|
|
|
This function efficiently applies tags based on card types using vectorized operations.
|
|
It handles special cases for different card types and maintains compatibility with
|
|
the existing tagging system.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info('Setting card type tags on %s_cards.csv', color)
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'type', 'themeTags'}
|
|
if not required_cols.issubset(df.columns):
|
|
raise ValueError(f"Missing required columns: {required_cols - set(df.columns)}")
|
|
|
|
# Define type-to-tag mapping
|
|
type_tag_map = settings.TYPE_TAG_MAPPING
|
|
|
|
# Process each card type
|
|
for card_type, tags in type_tag_map.items():
|
|
mask = tag_utils.create_type_mask(df, card_type)
|
|
if mask.any():
|
|
tag_utils.apply_tag_vectorized(df, mask, tags)
|
|
logger.info('Tagged %d cards with %s type', mask.sum(), card_type)
|
|
|
|
# Log completion
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Card type tagging completed in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error('Error in tag_for_card_types: %s', str(e))
|
|
raise
|
|
# Overwrite file with artifact tag added
|
|
logger.info(f'Card type tags set on {color}_cards.csv.')
|
|
|
|
## Add creature types to the theme tags
|
|
def add_creatures_to_tags(df: pd.DataFrame, color: str) -> None:
|
|
"""Add kindred tags to theme tags based on creature types using vectorized operations.
|
|
|
|
This function efficiently processes creature types and adds corresponding kindred tags
|
|
using pandas vectorized operations instead of row-by-row iteration.
|
|
|
|
Args:
|
|
df: DataFrame containing card data with creatureTypes and themeTags columns
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Adding creature types to theme tags in {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'creatureTypes', 'themeTags'}
|
|
missing = required_cols - set(df.columns)
|
|
if missing:
|
|
raise ValueError(f"Missing required columns: {missing}")
|
|
|
|
# Create mask for rows with non-empty creature types
|
|
has_creatures_mask = df['creatureTypes'].apply(lambda x: bool(x) if isinstance(x, list) else False)
|
|
|
|
if has_creatures_mask.any():
|
|
# Get rows with creature types
|
|
creature_rows = df[has_creatures_mask]
|
|
|
|
# Generate kindred tags vectorized
|
|
def add_kindred_tags(row):
|
|
current_tags = row['themeTags']
|
|
kindred_tags = [f"{ct} Kindred" for ct in row['creatureTypes']]
|
|
return sorted(list(set(current_tags + kindred_tags)))
|
|
|
|
# Update tags for matching rows
|
|
df.loc[has_creatures_mask, 'themeTags'] = creature_rows.apply(add_kindred_tags, axis=1)
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Added kindred tags to {has_creatures_mask.sum()} cards in {duration:.2f}s')
|
|
|
|
else:
|
|
logger.info('No cards with creature types found')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in add_creatures_to_tags: {str(e)}')
|
|
raise
|
|
|
|
logger.info(f'Creature types added to theme tags in {color}_cards.csv')
|
|
|
|
## Add keywords to theme tags
|
|
def tag_for_keywords(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards based on their keywords using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info('Tagging cards with keywords in %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for valid keywords
|
|
has_keywords = pd.notna(df['keywords'])
|
|
|
|
if has_keywords.any():
|
|
# Process cards with keywords
|
|
keywords_df = df[has_keywords].copy()
|
|
|
|
# Split keywords into lists
|
|
keywords_df['keyword_list'] = keywords_df['keywords'].str.split(', ')
|
|
|
|
# Add each keyword as a tag
|
|
for idx, row in keywords_df.iterrows():
|
|
if isinstance(row['keyword_list'], list):
|
|
current_tags = df.at[idx, 'themeTags']
|
|
new_tags = sorted(list(set(current_tags + row['keyword_list'])))
|
|
df.at[idx, 'themeTags'] = new_tags
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Tagged %d cards with keywords in %.2f seconds', has_keywords.sum(), duration)
|
|
|
|
except Exception as e:
|
|
logger.error('Error tagging keywords: %s', str(e))
|
|
raise
|
|
|
|
## Sort any set tags
|
|
def sort_theme_tags(df, color):
|
|
logger.info(f'Alphabetically sorting theme tags in {color}_cards.csv.')
|
|
|
|
df['themeTags'] = df['themeTags'].apply(tag_utils.sort_list)
|
|
|
|
columns_to_keep = ['name', 'faceName','edhrecRank', 'colorIdentity', 'colors', 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side']
|
|
df = df[columns_to_keep]
|
|
logger.info(f'Theme tags alphabetically sorted in {color}_cards.csv.')
|
|
|
|
### Cost reductions
|
|
def tag_for_cost_reduction(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that reduce spell costs using vectorized operations.
|
|
|
|
This function identifies cards that reduce casting costs through various means including:
|
|
- General cost reduction effects
|
|
- Artifact cost reduction
|
|
- Enchantment cost reduction
|
|
- Affinity and similar mechanics
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info('Tagging cost reduction cards in %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create masks for different cost reduction patterns
|
|
cost_mask = tag_utils.create_text_mask(df, PATTERN_GROUPS['cost_reduction'])
|
|
|
|
# Add specific named cards
|
|
named_cards = [
|
|
'Ancient Cellarspawn', 'Beluna Grandsquall', 'Cheering Fanatic',
|
|
'Cloud Key', 'Conduit of Ruin', 'Eluge, the Shoreless Sea',
|
|
'Goblin Anarchomancer', 'Goreclaw, Terror of Qal Sisma',
|
|
'Helm of Awakening', 'Hymn of the Wilds', 'It that Heralds the End',
|
|
'K\'rrik, Son of Yawgmoth', 'Killian, Ink Duelist', 'Krosan Drover',
|
|
'Memory Crystal', 'Myth Unbound', 'Mistform Warchief',
|
|
'Ranar the Ever-Watchful', 'Rowan, Scion of War', 'Semblence Anvil',
|
|
'Spectacle Mage', 'Spellwild Ouphe', 'Strong Back',
|
|
'Thryx, the Sudden Storm', 'Urza\'s Filter', 'Will, Scion of Peace',
|
|
'Will Kenrith'
|
|
]
|
|
named_mask = tag_utils.create_name_mask(df, named_cards)
|
|
|
|
# Combine masks
|
|
final_mask = cost_mask | named_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Cost Reduction'])
|
|
|
|
# Add spellslinger tags for noncreature spell cost reduction
|
|
spell_mask = final_mask & tag_utils.create_text_mask(df, r"Sorcery|Instant|noncreature")
|
|
tag_utils.apply_tag_vectorized(df, spell_mask, ['Spellslinger', 'Spells Matter'])
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Tagged %d cost reduction cards in %.2fs', final_mask.sum(), duration)
|
|
|
|
except Exception as e:
|
|
logger.error('Error tagging cost reduction cards: %s', str(e))
|
|
raise
|
|
|
|
### Card draw/advantage
|
|
## General card draw/advantage
|
|
def tag_for_card_draw(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that have card draw effects or care about drawing cards.
|
|
|
|
This function identifies and tags cards with various types of card draw effects including:
|
|
- Conditional draw (triggered/activated abilities)
|
|
- Looting effects (draw + discard)
|
|
- Cost-based draw (pay life/sacrifice)
|
|
- Replacement draw effects
|
|
- Wheel effects
|
|
- Unconditional draw
|
|
|
|
The function maintains proper tag hierarchy and ensures consistent application
|
|
of related tags like 'Card Draw', 'Spellslinger', etc.
|
|
|
|
Args:
|
|
df: DataFrame containing card data to process
|
|
color: Color identifier for logging purposes (e.g. 'white', 'blue')
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting card draw effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Process each type of draw effect
|
|
tag_for_conditional_draw(df, color)
|
|
logger.info('Completed conditional draw tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_loot_effects(df, color)
|
|
logger.info('Completed loot effects tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_cost_draw(df, color)
|
|
logger.info('Completed cost-based draw tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_replacement_draw(df, color)
|
|
logger.info('Completed replacement draw tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_wheels(df, color)
|
|
logger.info('Completed wheel effects tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_unconditional_draw(df, color)
|
|
logger.info('Completed unconditional draw tagging')
|
|
print('\n==========\n')
|
|
|
|
# Log completion and performance metrics
|
|
duration = pd.Timestamp.now() - start_time
|
|
logger.info(f'Completed all card draw tagging in {duration.total_seconds():.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_card_draw: {str(e)}')
|
|
raise
|
|
|
|
## Conditional card draw (i.e. Rhystic Study or Trouble In Pairs)
|
|
def create_unconditional_draw_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with unconditional draw effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
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_mask = tag_utils.create_text_mask(df, draw_patterns)
|
|
|
|
# Create exclusion mask for conditional effects
|
|
excluded_tags = settings.DRAW_RELATED_TAGS
|
|
tag_mask = tag_utils.create_tag_mask(df, excluded_tags)
|
|
|
|
# Create text-based exclusions
|
|
text_patterns = settings.DRAW_EXCLUSION_PATTERNS
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
return draw_mask & ~(tag_mask | text_mask)
|
|
|
|
def tag_for_unconditional_draw(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that have unconditional draw effects using vectorized operations.
|
|
|
|
This function identifies and tags cards that draw cards without conditions or
|
|
additional costs. It excludes cards that already have conditional draw tags
|
|
or specific keywords.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging unconditional draw effects in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for unconditional draw effects
|
|
draw_mask = create_unconditional_draw_mask(df)
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, draw_mask, ['Unconditional Draw', 'Card Draw'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {draw_mask.sum()} cards with unconditional draw effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging unconditional draw effects: {str(e)}')
|
|
raise
|
|
|
|
## Conditional card draw (i.e. Rhystic Study or Trouble In Pairs)
|
|
def create_conditional_draw_exclusion_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that should be excluded from conditional draw effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards should be excluded
|
|
"""
|
|
# Create tag-based exclusions
|
|
excluded_tags = settings.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_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
# Create name-based exclusions
|
|
excluded_names = ['relic vial', 'vexing bauble']
|
|
name_mask = tag_utils.create_name_mask(df, excluded_names)
|
|
|
|
return tag_mask | text_mask | name_mask
|
|
|
|
def create_conditional_draw_trigger_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with conditional draw triggers.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have trigger patterns
|
|
"""
|
|
# Build trigger patterns
|
|
trigger_patterns = []
|
|
for trigger in triggers:
|
|
# Permanent/creature/player triggers
|
|
trigger_patterns.extend([
|
|
f'{trigger} a permanent',
|
|
f'{trigger} a creature',
|
|
f'{trigger} a player',
|
|
f'{trigger} an opponent',
|
|
f'{trigger} another creature',
|
|
f'{trigger} enchanted player',
|
|
f'{trigger} one or more creatures',
|
|
f'{trigger} one or more other creatures',
|
|
f'{trigger} you'
|
|
])
|
|
|
|
# Name-based attack triggers
|
|
trigger_patterns.append(f'{trigger} .* attacks')
|
|
|
|
# Create trigger mask
|
|
trigger_mask = tag_utils.create_text_mask(df, trigger_patterns)
|
|
|
|
# Add other trigger patterns
|
|
other_patterns = ['created a token', 'draw a card for each']
|
|
other_mask = tag_utils.create_text_mask(df, other_patterns)
|
|
|
|
return trigger_mask | other_mask
|
|
|
|
def create_conditional_draw_effect_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with draw effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
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]
|
|
|
|
# Add token and 'draw for each' patterns
|
|
draw_patterns.extend([
|
|
'created a token.*draw',
|
|
'draw a card for each'
|
|
])
|
|
|
|
return df['text'].str.contains('|'.join(draw_patterns), case=False, na=False)
|
|
|
|
def tag_for_conditional_draw(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that have conditional draw effects using vectorized operations.
|
|
|
|
This function identifies and tags cards that draw cards based on triggers or conditions.
|
|
It handles various patterns including:
|
|
- Permanent/creature triggers
|
|
- Player-based triggers
|
|
- Token creation triggers
|
|
- 'Draw for each' effects
|
|
|
|
The function excludes cards that:
|
|
- Already have certain tags (Cycling, Imprint, etc.)
|
|
- Contain specific text patterns (annihilator, ravenous)
|
|
- Have specific names (relic vial, vexing bauble)
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging conditional draw effects in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create exclusion mask
|
|
exclusion_mask = create_conditional_draw_exclusion_mask(df)
|
|
|
|
# Create trigger mask
|
|
trigger_mask = create_conditional_draw_trigger_mask(df)
|
|
|
|
# Create draw effect mask
|
|
draw_patterns = [f'draw {num} card' for num in num_to_search]
|
|
|
|
# Add token and 'draw for each' patterns
|
|
draw_patterns.extend([
|
|
'created a token.*draw',
|
|
'draw a card for each'
|
|
])
|
|
|
|
draw_mask = tag_utils.create_text_mask(df, draw_patterns)
|
|
|
|
# Combine masks
|
|
final_mask = trigger_mask & draw_mask & ~exclusion_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Conditional Draw', 'Card Draw'])
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with conditional draw effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging conditional draw effects: {str(e)}')
|
|
raise
|
|
|
|
## Loot effects, I.E. draw a card, discard a card. Or discard a card, draw a card
|
|
def create_loot_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with standard loot effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have loot effects
|
|
"""
|
|
# Exclude cards that already have other loot-like effects
|
|
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]
|
|
discard_patterns = [
|
|
'discard the rest',
|
|
'for each card drawn this way, discard',
|
|
'if you do, discard',
|
|
'then discard'
|
|
]
|
|
|
|
has_draw = tag_utils.create_text_mask(df, draw_patterns)
|
|
has_discard = tag_utils.create_text_mask(df, discard_patterns)
|
|
|
|
return ~has_other_loot & has_draw & has_discard
|
|
|
|
def create_connive_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with connive effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have connive effects
|
|
"""
|
|
has_keyword = tag_utils.create_keyword_mask(df, 'Connive')
|
|
has_text = tag_utils.create_text_mask(df, 'connives?')
|
|
return has_keyword | has_text
|
|
|
|
def create_cycling_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with cycling effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have cycling effects
|
|
"""
|
|
has_keyword = tag_utils.create_keyword_mask(df, 'Cycling')
|
|
has_text = tag_utils.create_text_mask(df, 'cycling')
|
|
return has_keyword | has_text
|
|
|
|
def create_blood_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with blood token effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have blood token effects
|
|
"""
|
|
return tag_utils.create_text_mask(df, 'blood token')
|
|
|
|
def tag_for_loot_effects(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with loot-like effects using vectorized operations.
|
|
|
|
This function handles tagging of all loot-like effects including:
|
|
- Standard loot (draw + discard)
|
|
- Connive
|
|
- Cycling
|
|
- Blood tokens
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging loot-like effects in {color}_cards.csv')
|
|
|
|
# Create masks for each effect type
|
|
loot_mask = create_loot_mask(df)
|
|
connive_mask = create_connive_mask(df)
|
|
cycling_mask = create_cycling_mask(df)
|
|
blood_mask = create_blood_mask(df)
|
|
|
|
# Apply tags based on masks
|
|
if loot_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, loot_mask, ['Loot', 'Card Draw'])
|
|
logger.info(f'Tagged {loot_mask.sum()} cards with standard loot effects')
|
|
|
|
if connive_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, connive_mask, ['Connive', 'Loot', 'Card Draw'])
|
|
logger.info(f'Tagged {connive_mask.sum()} cards with connive effects')
|
|
|
|
if cycling_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, cycling_mask, ['Cycling', 'Loot', 'Card Draw'])
|
|
logger.info(f'Tagged {cycling_mask.sum()} cards with cycling effects')
|
|
|
|
if blood_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, blood_mask, ['Blood Tokens', 'Loot', 'Card Draw'])
|
|
logger.info(f'Tagged {blood_mask.sum()} cards with blood token effects')
|
|
|
|
logger.info('Completed tagging loot-like effects')
|
|
|
|
## Sacrifice or pay life to draw effects
|
|
def tag_for_cost_draw(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that draw cards by paying life or sacrificing permanents.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info('Tagging cost-based draw effects in %s_cards.csv', color)
|
|
|
|
# Split into life and sacrifice patterns
|
|
life_pattern = 'life: draw'
|
|
life_mask = df['text'].str.contains(life_pattern, case=False, na=False)
|
|
|
|
sac_patterns = [
|
|
r'sacrifice (?:a|an) (?:artifact|creature|permanent)(?:[^,]*),?[^,]*draw',
|
|
r'sacrifice [^:]+: draw',
|
|
r'sacrificed[^,]+, draw'
|
|
]
|
|
sac_mask = df['text'].str.contains('|'.join(sac_patterns), case=False, na=False, regex=True)
|
|
|
|
# Apply life draw tags
|
|
if life_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, life_mask, ['Life to Draw', 'Card Draw'])
|
|
logger.info('Tagged %d cards with life payment draw effects', life_mask.sum())
|
|
|
|
# Apply sacrifice draw tags
|
|
if sac_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, sac_mask, ['Sacrifice to Draw', 'Card Draw'])
|
|
logger.info('Tagged %d cards with sacrifice draw effects', sac_mask.sum())
|
|
|
|
logger.info('Completed tagging cost-based draw effects')
|
|
|
|
## Replacement effects, that might have you draw more cards
|
|
def create_replacement_draw_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with replacement draw effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have replacement draw effects
|
|
"""
|
|
# Create trigger patterns
|
|
trigger_patterns = []
|
|
for trigger in triggers:
|
|
trigger_patterns.extend([
|
|
f'{trigger} a player.*instead.*draw',
|
|
f'{trigger} an opponent.*instead.*draw',
|
|
f'{trigger} the beginning of your draw step.*instead.*draw',
|
|
f'{trigger} you.*instead.*draw'
|
|
])
|
|
|
|
# Create other replacement patterns
|
|
replacement_patterns = [
|
|
'if a player would.*instead.*draw',
|
|
'if an opponent would.*instead.*draw',
|
|
'if you would.*instead.*draw'
|
|
]
|
|
|
|
# Combine all patterns
|
|
all_patterns = '|'.join(trigger_patterns + replacement_patterns)
|
|
|
|
# Create base mask for replacement effects
|
|
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_mask = tag_utils.create_text_mask(df, number_patterns)
|
|
|
|
# Add mask for non-specific numbers
|
|
nonspecific_mask = tag_utils.create_text_mask(df, 'draw that many plus|draws that many plus') # df['text'].str.contains('draw that many plus|draws that many plus', case=False, na=False)
|
|
|
|
return base_mask & (number_mask | nonspecific_mask)
|
|
|
|
def create_replacement_draw_exclusion_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that should be excluded from replacement draw effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards should be excluded
|
|
"""
|
|
# Create tag-based exclusions
|
|
excluded_tags = settings.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_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
return tag_mask | text_mask
|
|
|
|
def tag_for_replacement_draw(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that have replacement draw effects using vectorized operations.
|
|
|
|
This function identifies and tags cards that modify or replace card draw effects,
|
|
such as drawing additional cards or replacing normal draw effects with other effects.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Example patterns tagged:
|
|
- Trigger-based replacement effects ("whenever you draw...instead")
|
|
- Conditional replacement effects ("if you would draw...instead")
|
|
- Specific card number replacements
|
|
- Non-specific card number replacements ("draw that many plus")
|
|
"""
|
|
logger.info(f'Tagging replacement draw effects in {color}_cards.csv')
|
|
|
|
try:
|
|
# Create replacement draw mask
|
|
replacement_mask = create_replacement_draw_mask(df)
|
|
|
|
# Create exclusion mask
|
|
exclusion_mask = create_replacement_draw_exclusion_mask(df)
|
|
|
|
# Add specific card names
|
|
specific_cards_mask = tag_utils.create_name_mask(df, 'sylvan library')
|
|
|
|
# Combine masks
|
|
final_mask = (replacement_mask & ~exclusion_mask) | specific_cards_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Replacement Draw', 'Card Draw'])
|
|
|
|
logger.info(f'Tagged {final_mask.sum()} cards with replacement draw effects')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging replacement draw effects: {str(e)}')
|
|
raise
|
|
|
|
logger.info(f'Completed tagging replacement draw effects in {color}_cards.csv')
|
|
|
|
## Wheels
|
|
def tag_for_wheels(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that have wheel effects or care about drawing/discarding cards.
|
|
|
|
This function identifies and tags cards that:
|
|
- Force excess draw and discard
|
|
- Have payoffs for drawing/discarding
|
|
- Care about wheel effects
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging "Wheel" effects in {color}_cards.csv')
|
|
|
|
try:
|
|
# Create masks for different wheel conditions
|
|
# Define text patterns for wheel effects
|
|
wheel_patterns = [
|
|
'an opponent draws a card',
|
|
'cards you\'ve drawn',
|
|
'draw your second card',
|
|
'draw that many cards',
|
|
'draws an additional card',
|
|
'draws a card',
|
|
'draws cards',
|
|
'draws half that many cards',
|
|
'draws their first second card',
|
|
'draws their second second card',
|
|
'draw two cards instead',
|
|
'draws two additional cards',
|
|
'discards that card',
|
|
'discards their hand, then draws',
|
|
'each card your opponents have drawn',
|
|
'each draw a card',
|
|
'each opponent draws a card',
|
|
'each player draws',
|
|
'has no cards in hand',
|
|
'have no cards in hand',
|
|
'may draw a card',
|
|
'maximum hand size',
|
|
'no cards in it, you win the game instead',
|
|
'opponent discards',
|
|
'you draw a card',
|
|
'whenever you draw a card'
|
|
]
|
|
wheel_cards = [
|
|
'arcane denial', 'bloodchief ascension', 'dark deal', 'elenda and azor', 'elixir of immortality',
|
|
'forced fruition', 'glunch, the bestower', 'kiora the rising tide', 'kynaios and tiro of meletis',
|
|
'library of leng','loran of the third path', 'mr. foxglove', 'raffine, scheming seer',
|
|
'sauron, the dark lord', 'seizan, perverter of truth', 'triskaidekaphile', 'twenty-toed toad',
|
|
'waste not', 'wedding ring', 'whispering madness'
|
|
]
|
|
|
|
text_mask = tag_utils.create_text_mask(df, wheel_patterns)
|
|
name_mask = tag_utils.create_name_mask(df, wheel_cards)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | name_mask
|
|
|
|
# Apply tags
|
|
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_mask = final_mask & df['text'].str.contains(trigger_pattern, case=False, na=False)
|
|
tag_utils.apply_tag_vectorized(df, trigger_mask, ['Draw Triggers'])
|
|
|
|
logger.info(f'Tagged {final_mask.sum()} cards with "Wheel" effects')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging "Wheel" effects: {str(e)}')
|
|
raise
|
|
|
|
### Artifacts
|
|
def tag_for_artifacts(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about Artifacts or are specific kinds of Artifacts
|
|
(i.e. Equipment or Vehicles).
|
|
|
|
This function identifies and tags cards with Artifact-related effects including:
|
|
- Creating Artifact tokens
|
|
- Casting Artifact spells
|
|
- Equipment
|
|
- Vehicles
|
|
|
|
The function maintains proper tag hierarchy and ensures consistent application
|
|
of related tags like 'Card Draw', 'Spellslinger', etc.
|
|
|
|
Args:
|
|
df: DataFrame containing card data to process
|
|
color: Color identifier for logging purposes (e.g. 'white', 'blue')
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting "Artifact" and "Artifacts Matter" tagging for {color}_cards.csv')
|
|
print('\n==========\n')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Process each type of draw effect
|
|
tag_for_artifact_tokens(df, color)
|
|
logger.info('Completed Artifact token tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_equipment(df, color)
|
|
logger.info('Completed Equipment tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_vehicles(df, color)
|
|
logger.info('Completed Vehicle tagging')
|
|
print('\n==========\n')
|
|
|
|
# Log completion and performance metrics
|
|
duration = pd.Timestamp.now() - start_time
|
|
logger.info(f'Completed all "Artifact" and "Artifacts Matter" tagging in {duration.total_seconds():.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_artifacts: {str(e)}')
|
|
raise
|
|
|
|
## Artifact Tokens
|
|
def tag_for_artifact_tokens(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that create or care about artifact tokens using vectorized operations.
|
|
|
|
This function handles tagging of:
|
|
- Generic artifact token creation
|
|
- Predefined artifact token types (Treasure, Food, etc)
|
|
- Fabricate keyword
|
|
|
|
The function applies both generic artifact token tags and specific token type tags
|
|
(e.g., 'Treasure Token', 'Food Token') based on the tokens created.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info('Setting artifact token tags on %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Tag generic artifact tokens
|
|
generic_mask = create_generic_artifact_mask(df)
|
|
if generic_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, generic_mask,
|
|
['Artifact Tokens', 'Artifacts Matter', 'Token Creation', 'Tokens Matter'])
|
|
logger.info('Tagged %d cards with generic artifact token effects', generic_mask.sum())
|
|
|
|
# Tag predefined artifact tokens
|
|
predefined_mask, token_map = create_predefined_artifact_mask(df)
|
|
if predefined_mask.any():
|
|
# Apply base artifact token tags
|
|
tag_utils.apply_tag_vectorized(df, predefined_mask,
|
|
['Artifact Tokens', 'Artifacts Matter', 'Token Creation', 'Tokens Matter'])
|
|
|
|
# Track token type counts
|
|
token_counts = {} # type: dict
|
|
|
|
# Apply specific token type tags
|
|
for idx, token_type in token_map.items():
|
|
specific_tag = f'{token_type} Token'
|
|
tag_utils.apply_tag_vectorized(df.loc[idx:idx], pd.Series([True], index=[idx]), [specific_tag])
|
|
token_counts[token_type] = token_counts.get(token_type, 0) + 1
|
|
|
|
# Log results with token type counts
|
|
logger.info('Tagged %d cards with predefined artifact tokens:', predefined_mask.sum())
|
|
for token_type, count in token_counts.items():
|
|
logger.info(' - %s: %d cards', token_type, count)
|
|
|
|
# Tag fabricate cards
|
|
fabricate_mask = create_fabricate_mask(df)
|
|
if fabricate_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, fabricate_mask,
|
|
['Artifact Tokens', 'Artifacts Matter', 'Token Creation', 'Tokens Matter'])
|
|
logger.info('Tagged %d cards with Fabricate', fabricate_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed artifact token tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error('Error in tag_for_artifact_tokens: %s', str(e))
|
|
raise
|
|
|
|
# Generic Artifact tokens, such as karnstructs, or artifact soldiers
|
|
def create_generic_artifact_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that create non-predefined artifact tokens.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards create generic artifact tokens
|
|
"""
|
|
# Exclude specific cards
|
|
excluded_cards = [
|
|
'diabolical salvation',
|
|
'lifecraft awakening',
|
|
'sandsteppe war riders',
|
|
'transmutation font'
|
|
]
|
|
name_exclusions = tag_utils.create_name_mask(df, excluded_cards)
|
|
|
|
# Create text pattern matches
|
|
create_pattern = r'create|put'
|
|
has_create = tag_utils.create_text_mask(df, create_pattern)
|
|
|
|
token_patterns = [
|
|
'artifact creature token',
|
|
'artifact token',
|
|
'construct artifact',
|
|
'copy of enchanted artifact',
|
|
'copy of target artifact',
|
|
'copy of that artifact'
|
|
]
|
|
has_token = tag_utils.create_text_mask(df, token_patterns)
|
|
|
|
# Named cards that create artifact tokens
|
|
named_cards = [
|
|
'bloodforged battle-axe', 'court of vantress', 'elmar, ulvenwald informant',
|
|
'faerie artisans', 'feldon of the third path', 'lenoardo da vinci',
|
|
'march of progress', 'nexus of becoming', 'osgir, the reconstructor',
|
|
'prototype portal', 'red sun\'s twilight', 'saheeli, the sun\'s brilliance',
|
|
'season of weaving', 'shaun, father of synths', 'sophia, dogged detective',
|
|
'vaultborn tyrant', 'wedding ring'
|
|
]
|
|
named_matches = tag_utils.create_name_mask(df, named_cards)
|
|
|
|
# Exclude fabricate cards
|
|
has_fabricate = tag_utils.create_text_mask(df, 'fabricate')
|
|
|
|
return (has_create & has_token & ~name_exclusions & ~has_fabricate) | named_matches
|
|
|
|
def create_predefined_artifact_mask(df: pd.DataFrame) -> tuple[pd.Series, dict[int, str]]:
|
|
"""Create a boolean mask for cards that create predefined artifact tokens and track token types.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Tuple containing:
|
|
- Boolean Series indicating which cards create predefined artifact tokens
|
|
- Dictionary mapping row indices to their matched token types
|
|
"""
|
|
# Create base mask for 'create' text
|
|
create_pattern = r'create|put'
|
|
has_create = tag_utils.create_text_mask(df, create_pattern)
|
|
|
|
# Initialize token mapping dictionary
|
|
token_map = {}
|
|
|
|
# Create masks for each token type
|
|
token_masks = []
|
|
|
|
for token in settings.artifact_tokens:
|
|
token_mask = tag_utils.create_text_mask(df, token.lower())
|
|
|
|
# Handle exclusions
|
|
if token == 'Blood':
|
|
token_mask &= df['name'] != 'Bloodroot Apothecary'
|
|
elif token == 'Gold':
|
|
token_mask &= ~df['name'].isin(['Goldspan Dragon', 'The Golden-Gear Colossus'])
|
|
elif token == 'Junk':
|
|
token_mask &= df['name'] != 'Junkyard Genius'
|
|
|
|
# Store token type for matching rows
|
|
matching_indices = df[token_mask].index
|
|
for idx in matching_indices:
|
|
if idx not in token_map: # Only store first match
|
|
token_map[idx] = token
|
|
|
|
token_masks.append(token_mask)
|
|
|
|
# Combine all token masks
|
|
final_mask = has_create & pd.concat(token_masks, axis=1).any(axis=1)
|
|
|
|
return final_mask, token_map
|
|
def create_fabricate_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with fabricate keyword.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have fabricate
|
|
"""
|
|
return tag_utils.create_text_mask(df, 'fabricate')
|
|
|
|
## Artifact Triggers
|
|
def create_artifact_triggers_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that care about artifacts.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards care about artifacts
|
|
"""
|
|
# Define artifact-related patterns
|
|
ability_patterns = [
|
|
'abilities of artifact', 'ability of artifact'
|
|
]
|
|
|
|
artifact_state_patterns = [
|
|
'are artifacts in addition', 'artifact enters', 'number of artifacts',
|
|
'number of other artifacts', 'number of tapped artifacts',
|
|
'number of artifact'
|
|
]
|
|
|
|
artifact_type_patterns = [
|
|
'all artifact', 'another artifact', 'another target artifact',
|
|
'artifact card', 'artifact creature you control',
|
|
'artifact creatures you control', 'artifact you control',
|
|
'artifacts you control', 'each artifact', 'target artifact'
|
|
]
|
|
|
|
casting_patterns = [
|
|
'affinity for artifacts', 'artifact spells as though they had flash',
|
|
'artifact spells you cast', 'cast an artifact', 'choose an artifact',
|
|
'whenever you cast a noncreature', 'whenever you cast an artifact'
|
|
]
|
|
|
|
counting_patterns = [
|
|
'mana cost among artifact', 'mana value among artifact',
|
|
'artifact with the highest mana value',
|
|
]
|
|
|
|
search_patterns = [
|
|
'search your library for an artifact'
|
|
]
|
|
|
|
trigger_patterns = [
|
|
'whenever a nontoken artifact', 'whenever an artifact',
|
|
'whenever another nontoken artifact', 'whenever one or more artifact'
|
|
]
|
|
|
|
# Combine all patterns
|
|
all_patterns = (
|
|
ability_patterns + artifact_state_patterns + artifact_type_patterns +
|
|
casting_patterns + counting_patterns + search_patterns + trigger_patterns +
|
|
['metalcraft', 'prowess', 'copy of any artifact']
|
|
)
|
|
|
|
# Create pattern string
|
|
pattern = '|'.join(all_patterns)
|
|
|
|
# Create mask
|
|
return df['text'].str.contains(pattern, case=False, na=False, regex=True)
|
|
|
|
def tag_for_artifact_triggers(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about artifacts using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Have abilities that trigger off artifacts
|
|
- Care about artifact states or counts
|
|
- Interact with artifact spells or permanents
|
|
- Have metalcraft or similar mechanics
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging cards that care about artifacts in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create artifact triggers mask
|
|
triggers_mask = create_artifact_triggers_mask(df)
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, triggers_mask, ['Artifacts Matter'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {triggers_mask.sum()} cards with artifact triggers in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging artifact triggers: {str(e)}')
|
|
raise
|
|
|
|
logger.info(f'Completed tagging cards that care about artifacts in {color}_cards.csv')
|
|
|
|
## Equipment
|
|
def create_equipment_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that are Equipment
|
|
|
|
This function identifies cards that:
|
|
- Have the Equipment subtype
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are Equipment
|
|
"""
|
|
# Create type-based mask
|
|
type_mask = tag_utils.create_type_mask(df, 'Equipment')
|
|
|
|
return type_mask
|
|
|
|
def create_equipment_cares_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that care about Equipment.
|
|
|
|
This function identifies cards that:
|
|
- Have abilities that trigger off Equipment
|
|
- Care about equipped creatures
|
|
- Modify Equipment or equipped creatures
|
|
- Have Equipment-related keywords
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards care about Equipment
|
|
"""
|
|
# Create text pattern mask
|
|
text_patterns = [
|
|
'equipment you control',
|
|
'equipped creature',
|
|
'attach',
|
|
'equip',
|
|
'equipment spells',
|
|
'equipment abilities',
|
|
'modified',
|
|
'reconfigure'
|
|
]
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
# Create keyword mask
|
|
keyword_patterns = ['Modified', 'Equip', 'Reconfigure']
|
|
keyword_mask = tag_utils.create_keyword_mask(df, keyword_patterns)
|
|
|
|
# Create specific cards mask
|
|
specific_cards = settings.EQUIPMENT_SPECIFIC_CARDS
|
|
name_mask = tag_utils.create_name_mask(df, specific_cards)
|
|
|
|
return text_mask | keyword_mask | name_mask
|
|
|
|
def tag_equipment(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that are Equipment or care about Equipment using vectorized operations.
|
|
|
|
This function identifies and tags:
|
|
- Equipment cards
|
|
- Cards that care about Equipment
|
|
- Cards with Equipment-related abilities
|
|
- Cards that modify Equipment or equipped creatures
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
logger.info('Tagging Equipment cards in %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create equipment mask
|
|
equipment_mask = create_equipment_mask(df)
|
|
if equipment_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, equipment_mask, ['Equipment', 'Equipment Matters', 'Voltron'])
|
|
logger.info('Tagged %d Equipment cards', equipment_mask.sum())
|
|
|
|
# Create equipment cares mask
|
|
cares_mask = create_equipment_cares_mask(df)
|
|
if cares_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, cares_mask,
|
|
['Artifacts Matter', 'Equipment Matters', 'Voltron'])
|
|
logger.info('Tagged %d cards that care about Equipment', cares_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed Equipment tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error('Error tagging Equipment cards: %s', str(e))
|
|
raise
|
|
|
|
## Vehicles
|
|
def create_vehicle_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that are Vehicles or care about Vehicles.
|
|
|
|
This function identifies cards that:
|
|
- Have the Vehicle subtype
|
|
- Have crew abilities
|
|
- Care about Vehicles or Pilots
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are Vehicles or care about them
|
|
"""
|
|
# Create type-based mask
|
|
type_mask = tag_utils.create_type_mask(df, ['Vehicle', 'Pilot'])
|
|
|
|
# Create text-based mask
|
|
text_patterns = [
|
|
'vehicle', 'crew', 'pilot',
|
|
]
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
return type_mask | text_mask
|
|
|
|
def tag_vehicles(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that are Vehicles or care about Vehicles using vectorized operations.
|
|
|
|
This function identifies and tags:
|
|
- Vehicle cards
|
|
- Pilot cards
|
|
- Cards that care about Vehicles
|
|
- Cards with crew abilities
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
logger.info('Tagging Vehicle cards in %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create vehicle mask
|
|
vehicle_mask = create_vehicle_mask(df)
|
|
if vehicle_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, vehicle_mask,
|
|
['Artifacts Matter', 'Vehicles'])
|
|
logger.info('Tagged %d Vehicle-related cards', vehicle_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed Vehicle tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error('Error tagging Vehicle cards: %s', str(e))
|
|
raise
|
|
|
|
### Enchantments
|
|
def tag_for_enchantments(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about Enchantments or are specific kinds of Enchantments
|
|
(i.e. Equipment or Vehicles).
|
|
|
|
This function identifies and tags cards with Enchantment-related effects including:
|
|
- Creating Enchantment tokens
|
|
- Casting Enchantment spells
|
|
- Auras
|
|
- Constellation
|
|
- Cases
|
|
- Rooms
|
|
- Classes
|
|
- Backrounds
|
|
- Shrines
|
|
|
|
The function maintains proper tag hierarchy and ensures consistent application
|
|
of related tags like 'Card Draw', 'Spellslinger', etc.
|
|
|
|
Args:
|
|
df: DataFrame containing card data to process
|
|
color: Color identifier for logging purposes (e.g. 'white', 'blue')
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting "Enchantment" and "Enchantments Matter" tagging for {color}_cards.csv')
|
|
print('\n==========\n')
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Process each type of enchantment effect
|
|
tag_for_enchantment_tokens(df, color)
|
|
logger.info('Completed Enchantment token tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_enchantments_matter(df, color)
|
|
logger.info('Completed "Enchantments Matter" tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_auras(df, color)
|
|
logger.info('Completed Aura tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_constellation(df, color)
|
|
logger.info('Completed Constellation tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_sagas(df, color)
|
|
logger.info('Completed Saga tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_cases(df, color)
|
|
logger.info('Completed Case tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_rooms(df, color)
|
|
logger.info('Completed Room tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_backgrounds(df, color)
|
|
logger.info('Completed Background tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_shrines(df, color)
|
|
logger.info('Completed Shrine tagging')
|
|
print('\n==========\n')
|
|
|
|
# Log completion and performance metrics
|
|
duration = pd.Timestamp.now() - start_time
|
|
logger.info(f'Completed all "Enchantment" and "Enchantments Matter" tagging in {duration.total_seconds():.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_artifacts: {str(e)}')
|
|
raise
|
|
|
|
## Enchantment tokens
|
|
def tag_for_enchantment_tokens(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that create or care about enchantment tokens using vectorized operations.
|
|
|
|
This function handles tagging of:
|
|
- Generic enchantmeny token creation
|
|
- Predefined enchantment token types (Roles, Shards, etc)
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info('Setting ehcantment token tags on %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Tag generic artifact tokens
|
|
generic_mask = create_generic_enchantment_mask(df)
|
|
if generic_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, generic_mask,
|
|
['Enchantment Tokens', 'Enchantments Matter', 'Token Creation', 'Tokens Matter'])
|
|
logger.info('Tagged %d cards with generic enchantment token effects', generic_mask.sum())
|
|
|
|
# Tag predefined artifact tokens
|
|
predefined_mask = create_predefined_enchantment_mask(df)
|
|
if predefined_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, predefined_mask,
|
|
['Enchantment Tokens', 'Enchantments Matter', 'Token Creation', 'Tokens Matter'])
|
|
logger.info('Tagged %d cards with predefined enchantment tokens', predefined_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed enchantment token tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error('Error in tag_for_enchantment_tokens: %s', str(e))
|
|
raise
|
|
|
|
def create_generic_enchantment_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that create non-predefined enchantment tokens.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards create generic enchantmnet tokens
|
|
"""
|
|
# Create text pattern matches
|
|
create_pattern = r'create|put'
|
|
has_create = tag_utils.create_text_mask(df, create_pattern)
|
|
|
|
token_patterns = [
|
|
'copy of enchanted enchantment',
|
|
'copy of target enchantment',
|
|
'copy of that enchantment',
|
|
'enchantment creature token',
|
|
'enchantment token'
|
|
]
|
|
has_token = tag_utils.create_text_mask(df, token_patterns)
|
|
|
|
# Named cards that create enchantment tokens
|
|
named_cards = [
|
|
'court of vantress',
|
|
'fellhide spiritbinder',
|
|
'hammer of purphoros'
|
|
]
|
|
named_matches = tag_utils.create_name_mask(df, named_cards)
|
|
|
|
return (has_create & has_token) | named_matches
|
|
|
|
def create_predefined_enchantment_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that create non-predefined enchantment tokens.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards create generic enchantmnet tokens
|
|
"""
|
|
# Create text pattern matches
|
|
has_create = df['text'].str.contains('create', case=False, na=False)
|
|
|
|
# Create masks for each token type
|
|
token_masks = []
|
|
for token in settings.enchantment_tokens:
|
|
token_mask = tag_utils.create_text_mask(df, token.lower())
|
|
|
|
token_masks.append(token_mask)
|
|
|
|
return has_create & pd.concat(token_masks, axis=1).any(axis=1)
|
|
|
|
## General enchantments matter
|
|
def tag_for_enchantments_matter(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about enchantments using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Have abilities that trigger off enchantments
|
|
- Care about enchantment states or counts
|
|
- Interact with enchantment spells or permanents
|
|
- Have constellation or similar mechanics
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging cards that care about enchantments in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create enchantment triggers mask
|
|
# Define enchantment-related patterns
|
|
ability_patterns = [
|
|
'abilities of enchantment', 'ability of enchantment'
|
|
]
|
|
|
|
state_patterns = [
|
|
'are enchantments in addition', 'enchantment enters'
|
|
]
|
|
|
|
type_patterns = [
|
|
'all enchantment', 'another enchantment', 'enchantment card',
|
|
'enchantment creature you control', 'enchantment creatures you control',
|
|
'enchantment you control', 'enchantments you control'
|
|
]
|
|
|
|
casting_patterns = [
|
|
'cast an enchantment', 'enchantment spells as though they had flash',
|
|
'enchantment spells you cast'
|
|
]
|
|
|
|
counting_patterns = [
|
|
'mana value among enchantment', 'number of enchantment'
|
|
]
|
|
|
|
search_patterns = [
|
|
'search your library for an enchantment'
|
|
]
|
|
|
|
trigger_patterns = [
|
|
'whenever a nontoken enchantment', 'whenever an enchantment',
|
|
'whenever another nontoken enchantment', 'whenever one or more enchantment'
|
|
]
|
|
|
|
# Combine all patterns
|
|
all_patterns = (
|
|
ability_patterns + state_patterns + type_patterns +
|
|
casting_patterns + counting_patterns + search_patterns + trigger_patterns
|
|
)
|
|
triggers_mask = tag_utils.create_text_mask(df, all_patterns)
|
|
|
|
# Create exclusion mask
|
|
exclusion_mask = tag_utils.create_name_mask(df, 'luxa river shrine')
|
|
|
|
# Combine masks
|
|
final_mask = triggers_mask & ~exclusion_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Enchantments Matter'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with enchantment triggers in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging enchantment triggers: {str(e)}')
|
|
raise
|
|
|
|
logger.info(f'Completed tagging cards that care about enchantments in {color}_cards.csv')
|
|
|
|
## Aura
|
|
def tag_auras(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that are Auras or care about Auras using vectorized operations.
|
|
|
|
This function identifies cards that:
|
|
- Have abilities that trigger off Auras
|
|
- Care about enchanted permanents
|
|
- Modify Auras or enchanted permanents
|
|
- Have Aura-related keywords
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
logger.info('Tagging Aura cards in %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create Aura mask
|
|
aura_mask = tag_utils.create_type_mask(df, 'Aura')
|
|
if aura_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, aura_mask,
|
|
['Auras', 'Enchantments Matter', 'Voltron'])
|
|
logger.info('Tagged %d Aura cards', aura_mask.sum())
|
|
|
|
# Create cares mask
|
|
text_patterns = [
|
|
'aura',
|
|
'aura enters',
|
|
'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)
|
|
if cares_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, cares_mask,
|
|
['Auras', 'Enchantments Matter', 'Voltron'])
|
|
logger.info('Tagged %d cards that care about Auras', cares_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed Aura tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error('Error tagging Aura cards: %s', str(e))
|
|
raise
|
|
|
|
## Constellation
|
|
def tag_constellation(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with Constellation using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging Constellation cards in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for constellation keyword
|
|
constellation_mask = tag_utils.create_keyword_mask(df, 'Constellation')
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, constellation_mask, ['Constellation', 'Enchantments Matter'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {constellation_mask.sum()} Constellation cards in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Constellation cards: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Constellation cards')
|
|
|
|
## Sagas
|
|
def tag_sagas(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with the Saga type using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: if required DataFramecolumns are missing
|
|
"""
|
|
logger.info('Tagging Saga cards in %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for Saga type
|
|
saga_mask = tag_utils.create_type_mask(df, 'Saga')
|
|
if saga_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, saga_mask,
|
|
['Enchantments Matter', 'Sagas Matter'])
|
|
logger.info('Tagged %d Saga cards', saga_mask.sum())
|
|
|
|
# Create mask for cards that care about Sagas
|
|
text_patterns = [
|
|
'saga',
|
|
'put a saga',
|
|
'final chapter',
|
|
'lore counter'
|
|
]
|
|
cares_mask = tag_utils.create_text_mask(df, text_patterns) # create_saga_cares_mask(df)
|
|
if cares_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, cares_mask,
|
|
['Enchantments Matter', 'Sagas Matter'])
|
|
logger.info('Tagged %d cards that care about Sagas', cares_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed Saga tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Saga cards: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Saga cards')
|
|
|
|
## Cases
|
|
def tag_cases(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with the Case subtype using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: if required DataFramecolumns are missing
|
|
"""
|
|
logger.info('Tagging Case cards in %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for Case type
|
|
saga_mask = tag_utils.create_type_mask(df, 'Case')
|
|
if saga_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, saga_mask,
|
|
['Enchantments Matter', 'Cases Matter'])
|
|
logger.info('Tagged %d Saga cards', saga_mask.sum())
|
|
|
|
# Create Case cares_mask
|
|
cares_mask = tag_utils.create_text_mask(df, 'solve a case')
|
|
if cares_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, cares_mask,
|
|
['Enchantments Matter', 'Cases Matter'])
|
|
logger.info('Tagged %d cards that care about Cases', cares_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed Case tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Case cards: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Case cards')
|
|
|
|
## Rooms
|
|
def tag_rooms(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with the room subtype using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: if required DataFramecolumns are missing
|
|
"""
|
|
logger.info('Tagging Room cards in %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for Room type
|
|
room_mask = tag_utils.create_type_mask(df, 'Room')
|
|
if room_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, room_mask,
|
|
['Enchantments Matter', 'Rooms Matter'])
|
|
logger.info('Tagged %d Room cards', room_mask.sum())
|
|
|
|
# Create keyword mask for rooms
|
|
keyword_mask = tag_utils.create_keyword_mask(df, 'Eerie')
|
|
if keyword_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, keyword_mask,
|
|
['Enchantments Matter', 'Rooms Matter'])
|
|
|
|
# Create rooms care mask
|
|
cares_mask = tag_utils.create_text_mask(df, 'target room')
|
|
if cares_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, cares_mask,
|
|
['Enchantments Matter', 'Rooms Matter'])
|
|
logger.info('Tagged %d cards that care about Rooms', cares_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed Room tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Room cards: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Room cards')
|
|
|
|
## Classes
|
|
def tag_classes(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with the Class subtype using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: if required DataFramecolumns are missing
|
|
"""
|
|
logger.info('Tagging Class cards in %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for class type
|
|
class_mask = tag_utils.create_type_mask(df, 'Class')
|
|
if class_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, class_mask,
|
|
['Enchantments Matter', 'Classes Matter'])
|
|
logger.info('Tagged %d Class cards', class_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed Class tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Class cards: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Class cards')
|
|
|
|
## Background
|
|
def tag_backgrounds(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with the Background subtype or which let you choose a background using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: if required DataFramecolumns are missing
|
|
"""
|
|
logger.info('Tagging Background cards in %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for background type
|
|
class_mask = tag_utils.create_type_mask(df, 'Background')
|
|
if class_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, class_mask,
|
|
['Enchantments Matter', 'Backgrounds Matter'])
|
|
logger.info('Tagged %d Background cards', class_mask.sum())
|
|
|
|
# Create mask for Choose a Background
|
|
cares_mask = tag_utils.create_text_mask(df, 'Background')
|
|
if cares_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, cares_mask,
|
|
['Enchantments Matter', 'Backgroundss Matter'])
|
|
logger.info('Tagged %d cards that have Choose a Background', cares_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed Background tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Background cards: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Background cards')
|
|
|
|
## Shrines
|
|
def tag_shrines(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with the Shrine subtype using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: if required DataFramecolumns are missing
|
|
"""
|
|
logger.info('Tagging Shrine cards in %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for shrine type
|
|
class_mask = tag_utils.create_type_mask(df, 'Shrine')
|
|
if class_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, class_mask,
|
|
['Enchantments Matter', 'Shrines Matter'])
|
|
logger.info('Tagged %d Shrine cards', class_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed Shrine tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Shrine cards: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Shrine cards')
|
|
|
|
### Exile Matters
|
|
## Exile Matter effects, such as Impuse draw, foretell, etc...
|
|
def tag_for_exile_matters(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about exiling cards and casting them from exile.
|
|
|
|
This function identifies and tags cards with cast-from exile effects such as:
|
|
- Cascade
|
|
- Discover
|
|
- Foretell
|
|
- Imprint
|
|
- Impulse
|
|
- Plot
|
|
- Susend
|
|
|
|
The function maintains proper tag hierarchy and ensures consistent application
|
|
of related tags like 'Card Draw', 'Spellslinger', etc.
|
|
|
|
Args:
|
|
df: DataFrame containing card data to process
|
|
color: Color identifier for logging purposes (e.g. 'white', 'blue')
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting "Exile Matters" tagging for {color}_cards.csv')
|
|
print('\n==========\n')
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Process each type of Exile matters effect
|
|
tag_for_general_exile_matters(df, color)
|
|
logger.info('Completed general Exile Matters tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_cascade(df, color)
|
|
logger.info('Completed Cascade tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_discover(df, color)
|
|
logger.info('Completed Disxover tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_foretell(df, color)
|
|
logger.info('Completed Foretell tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_imprint(df, color)
|
|
logger.info('Completed Imprint tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_impulse(df, color)
|
|
logger.info('Completed Impulse tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_plot(df, color)
|
|
logger.info('Completed Plot tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_suspend(df, color)
|
|
logger.info('Completed Suspend tagging')
|
|
print('\n==========\n')
|
|
|
|
|
|
# Log completion and performance metrics
|
|
duration = pd.Timestamp.now() - start_time
|
|
logger.info(f'Completed all "Exile Matters" tagging in {duration.total_seconds():.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_exile_matters: {str(e)}')
|
|
raise
|
|
|
|
def tag_for_general_exile_matters(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that have a general care about casting from Exile theme.
|
|
|
|
This function identifies cards that:
|
|
- Trigger off casting a card from exile
|
|
- Trigger off playing a land from exile
|
|
- Putting cards into exile to later play
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purpposes
|
|
|
|
Raises:
|
|
ValueError: if required DataFrame columns are missing
|
|
"""
|
|
logger.info('Tagging Exile Matters cards in %s_cards.csv', color)
|
|
start_time =pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create exile mask
|
|
text_patterns = [
|
|
'cards in exile',
|
|
'cast a spell from exile',
|
|
'cast but don\'t own',
|
|
'cast from exile',
|
|
'casts a spell from exile',
|
|
'control but don\'t own',
|
|
'exiled with',
|
|
'from anywhere but their hand',
|
|
'from anywhere but your hand',
|
|
'from exile',
|
|
'own in exile',
|
|
'play a card from exile',
|
|
'plays a card from exile',
|
|
'play a land from exile',
|
|
'plays a land from exile',
|
|
'put into exile',
|
|
'remains exiled'
|
|
]
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
if text_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, text_mask, ['Exile Matters'])
|
|
logger.info('Tagged %d Exile Matters cards', text_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed Exile Matters tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error('Error tagging Exile Matters cards: %s', str(e))
|
|
raise
|
|
|
|
## Cascade cards
|
|
def tag_for_cascade(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that have or otherwise give the Cascade ability
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
logger.info('Tagging Cascade cards in %s_cards.csv', color)
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create Cascade mask
|
|
text_patterns = [
|
|
'gain cascade',
|
|
'has cascade',
|
|
'have cascade',
|
|
'have "cascade',
|
|
'with cascade',
|
|
]
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
if text_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, text_mask, ['Cascade', 'Exile Matters'])
|
|
logger.info('Tagged %d cards relating to Cascade', text_mask.sum())
|
|
|
|
keyword_mask = tag_utils.create_keyword_mask(df, 'Cascade')
|
|
if keyword_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, text_mask, ['Cascade', 'Exile Matters'])
|
|
logger.info('Tagged %d cards that have Cascade', keyword_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed Cascade tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error('Error tagging Cacade cards: %s', str(e))
|
|
raise
|
|
|
|
## Dsicover cards
|
|
def tag_for_discover(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with Discover using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging Discover cards in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for Discover keyword
|
|
keyword_mask = tag_utils.create_keyword_mask(df, 'Discover')
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, keyword_mask, ['Discover', 'Exile Matters'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {keyword_mask.sum()} Discover cards in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Discover cards: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Discover cards')
|
|
|
|
## Foretell cards, and cards that care about foretell
|
|
def tag_for_foretell(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with Foretell using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging Foretell cards in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for Foretell keyword
|
|
keyword_mask = tag_utils.create_keyword_mask(df, 'Foretell')
|
|
|
|
# Create mask for Foretell text
|
|
text_mask = tag_utils.create_text_mask(df, 'Foretell')
|
|
|
|
final_mask = keyword_mask | text_mask
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Foretell', 'Exile Matters'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} Foretell cards in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Foretell cards: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Foretell cards')
|
|
|
|
## Cards that have or care about imprint
|
|
def tag_for_imprint(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with Imprint using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging Imprint cards in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for Imprint keyword
|
|
keyword_mask = tag_utils.create_keyword_mask(df, 'Imprint')
|
|
|
|
# Create mask for Imprint text
|
|
text_mask = tag_utils.create_text_mask(df, 'Imprint')
|
|
|
|
final_mask = keyword_mask | text_mask
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Imprint', 'Exile Matters'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} Imprint cards in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Imprint cards: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Imprint cards')
|
|
|
|
## Cards that have or care about impulse
|
|
def create_impulse_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with impulse-like effects.
|
|
|
|
This function identifies cards that exile cards from the top of libraries
|
|
and allow playing them for a limited time, including:
|
|
- Exile top card(s) with may cast/play effects
|
|
- Named cards with similar effects
|
|
- Junk token creation
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have Impulse effects
|
|
"""
|
|
# Define text patterns
|
|
exile_patterns = [
|
|
'exile the top',
|
|
'exiles the top'
|
|
]
|
|
|
|
play_patterns = [
|
|
'may cast',
|
|
'may play'
|
|
]
|
|
|
|
# Named cards with Impulse effects
|
|
impulse_cards = [
|
|
'daxos of meletis', 'bloodsoaked insight', 'florian, voldaren scion',
|
|
'possibility storm', 'ragava, nimble pilferer', 'rakdos, the muscle',
|
|
'stolen strategy', 'urabrask, heretic praetor', 'valakut exploration',
|
|
'wild wasteland'
|
|
]
|
|
|
|
# Create exclusion patterns
|
|
exclusion_patterns = [
|
|
'damage to each', 'damage to target', 'deals combat damage',
|
|
'raid', 'target opponent\'s hand',
|
|
]
|
|
secondary_exclusion_patterns = [
|
|
'each opponent', 'morph', 'opponent\'s library',
|
|
'skip your draw', 'target opponent', 'that player\'s',
|
|
'you may look at the top card'
|
|
]
|
|
|
|
# Create masks
|
|
tag_mask = tag_utils.create_tag_mask(df, 'Imprint')
|
|
exile_mask = tag_utils.create_text_mask(df, exile_patterns)
|
|
play_mask = tag_utils.create_text_mask(df, play_patterns)
|
|
named_mask = tag_utils.create_name_mask(df, impulse_cards)
|
|
junk_mask = tag_utils.create_text_mask(df, 'junk token')
|
|
first_exclusion_mask = tag_utils.create_text_mask(df, exclusion_patterns)
|
|
planeswalker_mask = df['type'].str.contains('Planeswalker', case=False, na=False)
|
|
second_exclusion_mask = tag_utils.create_text_mask(df, secondary_exclusion_patterns)
|
|
exclusion_mask = (~first_exclusion_mask & ~planeswalker_mask) & second_exclusion_mask
|
|
|
|
# Combine masks
|
|
impulse_mask = ((exile_mask & play_mask & ~exclusion_mask & ~tag_mask) |
|
|
named_mask | junk_mask)
|
|
|
|
return impulse_mask
|
|
|
|
def tag_for_impulse(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that have impulse-like effects using vectorized operations.
|
|
|
|
This function identifies and tags cards that exile cards from library tops
|
|
and allow playing them for a limited time, including:
|
|
- Exile top card(s) with may cast/play effects
|
|
- Named cards with similar effects
|
|
- Junk token creation
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging Impulse effects in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create impulse mask
|
|
impulse_mask = create_impulse_mask(df)
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, impulse_mask, ['Exile Matters', 'Impulse'])
|
|
|
|
# Add Junk Tokens tag where applicable
|
|
junk_mask = impulse_mask & tag_utils.create_text_mask(df, 'junk token')
|
|
tag_utils.apply_tag_vectorized(df, junk_mask, ['Junk Tokens'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {impulse_mask.sum()} cards with Impulse effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Impulse effects: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Impulse effects')
|
|
## Cards that have or care about plotting
|
|
def tag_for_plot(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with Plot using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging Plot cards in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for Plot keyword
|
|
keyword_mask = tag_utils.create_keyword_mask(df, 'Plot')
|
|
|
|
# Create mask for Plot keyword
|
|
text_mask = tag_utils.create_text_mask(df, 'Plot')
|
|
|
|
final_mask = keyword_mask | text_mask
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Plot', 'Exile Matters'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} Plot cards in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Plot cards: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Plot cards')
|
|
|
|
## Cards that have or care about suspend
|
|
def tag_for_suspend(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with Suspend using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging Suspend cards in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for Suspend keyword
|
|
keyword_mask = tag_utils.create_keyword_mask(df, 'Suspend')
|
|
|
|
# Create mask for Suspend keyword
|
|
text_mask = tag_utils.create_text_mask(df, 'Suspend')
|
|
|
|
final_mask = keyword_mask | text_mask
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Suspend', 'Exile Matters'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} Suspend cards in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Suspend cards: {str(e)}')
|
|
raise
|
|
|
|
logger.info('Completed tagging Suspend cards')
|
|
|
|
### Tokens
|
|
def create_creature_token_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that create creature tokens.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards create creature tokens
|
|
"""
|
|
# Create base pattern for token creation
|
|
create_pattern = r'create|put'
|
|
has_create = tag_utils.create_text_mask(df, create_pattern)
|
|
|
|
# Create pattern for creature tokens
|
|
token_patterns = [
|
|
'artifact creature token',
|
|
'creature token',
|
|
'enchantment creature token'
|
|
]
|
|
has_token = tag_utils.create_text_mask(df, token_patterns)
|
|
|
|
# Create exclusion mask
|
|
exclusion_patterns = ['fabricate', 'modular']
|
|
exclusion_mask = tag_utils.create_text_mask(df, exclusion_patterns)
|
|
|
|
# Create name exclusion mask
|
|
excluded_cards = ['agatha\'s soul cauldron']
|
|
name_exclusions = tag_utils.create_name_mask(df, excluded_cards)
|
|
|
|
return has_create & has_token & ~exclusion_mask & ~name_exclusions
|
|
|
|
def create_token_modifier_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that modify token creation.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards modify token creation
|
|
"""
|
|
# Create patterns for token modification
|
|
modifier_patterns = [
|
|
'create one or more',
|
|
'one or more creature',
|
|
'one or more tokens would be created',
|
|
'one or more tokens would be put',
|
|
'one or more tokens would enter',
|
|
'one or more tokens you control',
|
|
'put one or more'
|
|
]
|
|
has_modifier = tag_utils.create_text_mask(df, modifier_patterns)
|
|
|
|
# Create patterns for token effects
|
|
effect_patterns = ['instead', 'plus']
|
|
has_effect = tag_utils.create_text_mask(df, effect_patterns)
|
|
|
|
# Create name exclusion mask
|
|
excluded_cards = [
|
|
'cloakwood swarmkeeper',
|
|
'neyali, sun\'s vanguard',
|
|
'staff of the storyteller'
|
|
]
|
|
name_exclusions = tag_utils.create_name_mask(df, excluded_cards)
|
|
|
|
return has_modifier & has_effect & ~name_exclusions
|
|
|
|
def create_tokens_matter_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that care about tokens.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards care about tokens
|
|
"""
|
|
# Create patterns for token matters
|
|
text_patterns = [
|
|
'tokens.*you.*control',
|
|
'that\'s a token',
|
|
]
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
return text_mask
|
|
|
|
def tag_for_tokens(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that create or modify tokens using vectorized operations.
|
|
|
|
This function identifies and tags:
|
|
- Cards that create creature tokens
|
|
- Cards that modify token creation (doublers, replacement effects)
|
|
- Cards that care about tokens
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info('Tagging token-related cards in %s_cards.csv', color)
|
|
print('\n==========\n')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create creature token mask
|
|
creature_mask = create_creature_token_mask(df)
|
|
if creature_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, creature_mask,
|
|
['Creature Tokens', 'Token Creation', 'Tokens Matter'])
|
|
logger.info('Tagged %d cards that create creature tokens', creature_mask.sum())
|
|
|
|
# Create token modifier mask
|
|
modifier_mask = create_token_modifier_mask(df)
|
|
if modifier_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, modifier_mask,
|
|
['Token Modification', 'Token Creation', 'Tokens Matter'])
|
|
logger.info('Tagged %d cards that modify token creation', modifier_mask.sum())
|
|
|
|
# Create tokens matter mask
|
|
matters_mask = create_tokens_matter_mask(df)
|
|
if matters_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, matters_mask,
|
|
['Tokens Matter'])
|
|
logger.info('Tagged %d cards that care about tokens', modifier_mask.sum())
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info('Completed token tagging in %.2fs', duration)
|
|
|
|
except Exception as e:
|
|
logger.error('Error tagging token cards: %s', str(e))
|
|
raise
|
|
|
|
### Life Matters
|
|
def tag_for_life_matters(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about life totals, life gain/loss, and related effects using vectorized operations.
|
|
|
|
This function coordinates multiple subfunctions to handle different life-related aspects:
|
|
- Lifegain effects and triggers
|
|
- Lifelink and lifelink-like abilities
|
|
- Life loss triggers and effects
|
|
- Food token creation and effects
|
|
- Life-related kindred synergies
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting "Life Matters" tagging for {color}_cards.csv')
|
|
print('\n==========\n')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'type', 'creatureTypes'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Process each type of life effect
|
|
tag_for_lifegain(df, color)
|
|
logger.info('Completed lifegain tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_lifelink(df, color)
|
|
logger.info('Completed lifelink tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_life_loss(df, color)
|
|
logger.info('Completed life loss tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_food(df, color)
|
|
logger.info('Completed food token tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_life_kindred(df, color)
|
|
logger.info('Completed life kindred tagging')
|
|
print('\n==========\n')
|
|
|
|
# Log completion and performance metrics
|
|
duration = pd.Timestamp.now() - start_time
|
|
logger.info(f'Completed all "Life Matters" tagging in {duration.total_seconds():.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_life_matters: {str(e)}')
|
|
raise
|
|
|
|
def tag_for_lifegain(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with lifegain effects using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging lifegain effects in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
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.extend(['gain life', 'gains life'])
|
|
|
|
gain_mask = tag_utils.create_text_mask(df, gain_patterns)
|
|
|
|
# Exclude replacement effects
|
|
replacement_mask = tag_utils.create_text_mask(df, ['if you would gain life', 'whenever you gain life'])
|
|
|
|
# Apply lifegain tags
|
|
final_mask = gain_mask & ~replacement_mask
|
|
if final_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Lifegain', 'Life Matters'])
|
|
logger.info(f'Tagged {final_mask.sum()} cards with lifegain effects')
|
|
|
|
# Tag lifegain triggers
|
|
trigger_mask = tag_utils.create_text_mask(df, ['if you would gain life', 'whenever you gain life'])
|
|
if trigger_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, trigger_mask, ['Lifegain', 'Lifegain Triggers', 'Life Matters'])
|
|
logger.info(f'Tagged {trigger_mask.sum()} cards with lifegain triggers')
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Completed lifegain tagging in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging lifegain effects: {str(e)}')
|
|
raise
|
|
|
|
def tag_for_lifelink(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with lifelink and lifelink-like effects using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging lifelink effects in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create masks for different lifelink patterns
|
|
lifelink_mask = tag_utils.create_text_mask(df, 'lifelink')
|
|
lifelike_mask = tag_utils.create_text_mask(df, [
|
|
'deals damage, you gain that much life',
|
|
'loses life.*gain that much life'
|
|
])
|
|
|
|
# Exclude combat damage references for life loss conversion
|
|
damage_mask = tag_utils.create_text_mask(df, 'deals damage')
|
|
life_loss_mask = lifelike_mask & ~damage_mask
|
|
|
|
# Combine masks
|
|
final_mask = lifelink_mask | lifelike_mask | life_loss_mask
|
|
|
|
# Apply tags
|
|
if final_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Lifelink', 'Lifegain', 'Life Matters'])
|
|
logger.info(f'Tagged {final_mask.sum()} cards with lifelink effects')
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Completed lifelink tagging in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging lifelink effects: {str(e)}')
|
|
raise
|
|
|
|
def tag_for_life_loss(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about life loss using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging life loss effects in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create masks for different life loss patterns
|
|
text_patterns = [
|
|
'you lost life',
|
|
'you gained and lost life',
|
|
'you gained or lost life',
|
|
'you would lose life',
|
|
'you\'ve gained and lost life this turn',
|
|
'you\'ve lost life',
|
|
'whenever you gain or lose life',
|
|
'whenever you lose life'
|
|
]
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
# Apply tags
|
|
if text_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, text_mask, ['Lifeloss', 'Lifeloss Triggers', 'Life Matters'])
|
|
logger.info(f'Tagged {text_mask.sum()} cards with life loss effects')
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Completed life loss tagging in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging life loss effects: {str(e)}')
|
|
raise
|
|
|
|
def tag_for_food(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that create or care about Food using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging Food token in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create masks for Food tokens
|
|
text_mask = tag_utils.create_text_mask(df, 'food')
|
|
type_mask = tag_utils.create_type_mask(df, 'food')
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | type_mask
|
|
|
|
# Apply tags
|
|
if final_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Food', 'Lifegain', 'Life Matters'])
|
|
logger.info(f'Tagged {final_mask.sum()} cards with Food effects')
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Completed Food tagging in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Food effects: {str(e)}')
|
|
raise
|
|
|
|
def tag_for_life_kindred(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with life-related kindred synergies using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging life-related kindred effects in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create mask for life-related creature types
|
|
life_tribes = ['Angel', 'Bat', 'Cleric', 'Vampire']
|
|
kindred_mask = df['creatureTypes'].apply(lambda x: any(tribe in x for tribe in life_tribes))
|
|
|
|
# Apply tags
|
|
if kindred_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, kindred_mask, ['Lifegain', 'Life Matters'])
|
|
logger.info(f'Tagged {kindred_mask.sum()} cards with life-related kindred effects')
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Completed life kindred tagging in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging life kindred effects: {str(e)}')
|
|
raise
|
|
|
|
### Counters
|
|
def tag_for_counters(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about or interact with counters using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Add or remove counters (+1/+1, -1/-1, special counters)
|
|
- Care about counters being placed or removed
|
|
- Have counter-based abilities (proliferate, undying, etc)
|
|
- Create or modify counters
|
|
|
|
The function maintains proper tag hierarchy and ensures consistent application
|
|
of related tags like 'Counters Matter', '+1/+1 Counters', etc.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting counter-related tagging for {color}_cards.csv')
|
|
print('\n==========\n')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'name', 'creatureTypes'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Process each type of counter effect
|
|
tag_for_general_counters(df, color)
|
|
logger.info('Completed general counter tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_plus_counters(df, color)
|
|
logger.info('Completed +1/+1 counter tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_minus_counters(df, color)
|
|
logger.info('Completed -1/-1 counter tagging')
|
|
print('\n==========\n')
|
|
|
|
tag_for_special_counters(df, color)
|
|
logger.info('Completed special counter tagging')
|
|
print('\n==========\n')
|
|
|
|
# Log completion and performance metrics
|
|
duration = pd.Timestamp.now() - start_time
|
|
logger.info(f'Completed all counter-related tagging in {duration.total_seconds():.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_counters: {str(e)}')
|
|
raise
|
|
|
|
def tag_for_general_counters(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about counters in general using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging general counter effects in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create masks for different counter patterns
|
|
text_patterns = [
|
|
'choose a kind of counter',
|
|
'if it had counters',
|
|
'move a counter',
|
|
'one or more counters',
|
|
'proliferate',
|
|
'remove a counter',
|
|
'with counters on them'
|
|
]
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
# Create mask for specific cards
|
|
specific_cards = [
|
|
'banner of kinship',
|
|
'damning verdict',
|
|
'ozolith'
|
|
]
|
|
name_mask = tag_utils.create_name_mask(df, specific_cards)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | name_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Counters Matter'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with general counter effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging general counter effects: {str(e)}')
|
|
raise
|
|
|
|
def tag_for_plus_counters(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about +1/+1 counters using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging +1/+1 counter effects in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create text pattern mask
|
|
text_patterns = [
|
|
r'\+1/\+1 counter',
|
|
r'if it had counters',
|
|
r'one or more counters',
|
|
r'one or more \+1/\+1 counter',
|
|
r'proliferate',
|
|
r'undying',
|
|
r'with counters on them'
|
|
]
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
# Create creature type mask
|
|
type_mask = df['creatureTypes'].apply(lambda x: 'Hydra' in x if isinstance(x, list) else False)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | type_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['+1/+1 Counters', 'Counters Matter', 'Voltron'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with +1/+1 counter effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging +1/+1 counter effects: {str(e)}')
|
|
raise
|
|
|
|
def tag_for_minus_counters(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about -1/-1 counters using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging -1/-1 counter effects in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Create text pattern mask
|
|
text_patterns = [
|
|
'-1/-1 counter',
|
|
'if it had counters',
|
|
'infect',
|
|
'one or more counter',
|
|
'one or more -1/-1 counter',
|
|
'persist',
|
|
'proliferate',
|
|
'wither'
|
|
]
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, text_mask, ['-1/-1 Counters', 'Counters Matter'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {text_mask.sum()} cards with -1/-1 counter effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging -1/-1 counter effects: {str(e)}')
|
|
raise
|
|
|
|
def tag_for_special_counters(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about special counters using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
"""
|
|
logger.info(f'Tagging special counter effects in {color}_cards.csv')
|
|
start_time = pd.Timestamp.now()
|
|
|
|
try:
|
|
# Process each counter type
|
|
counter_counts = {}
|
|
for counter_type in settings.counter_types:
|
|
# Create pattern for this counter type
|
|
pattern = f'{counter_type} counter'
|
|
mask = tag_utils.create_text_mask(df, pattern)
|
|
|
|
if mask.any():
|
|
# Apply tags
|
|
tags = [f'{counter_type} Counters', 'Counters Matter']
|
|
tag_utils.apply_tag_vectorized(df, mask, tags)
|
|
counter_counts[counter_type] = mask.sum()
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
total_cards = sum(counter_counts.values())
|
|
logger.info(f'Tagged {total_cards} cards with special counter effects in {duration:.2f}s')
|
|
for counter_type, count in counter_counts.items():
|
|
if count > 0:
|
|
logger.info(f' - {counter_type}: {count} cards')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging special counter effects: {str(e)}')
|
|
raise
|
|
|
|
### Voltron
|
|
def create_voltron_commander_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that are Voltron commanders.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are Voltron commanders
|
|
"""
|
|
return tag_utils.create_name_mask(df, settings.VOLTRON_COMMANDER_CARDS)
|
|
|
|
def create_voltron_support_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that support Voltron strategies.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards support Voltron strategies
|
|
"""
|
|
return tag_utils.create_text_mask(df, settings.VOLTRON_PATTERNS)
|
|
|
|
def create_voltron_equipment_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for Equipment-based Voltron cards.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are Equipment-based Voltron cards
|
|
"""
|
|
return tag_utils.create_type_mask(df, 'Equipment')
|
|
|
|
def create_voltron_aura_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for Aura-based Voltron cards.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are Aura-based Voltron cards
|
|
"""
|
|
return tag_utils.create_type_mask(df, 'Aura')
|
|
|
|
def tag_for_voltron(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that fit the Voltron strategy.
|
|
|
|
This function identifies and tags cards that support the Voltron strategy including:
|
|
- Voltron commanders
|
|
- Equipment and Auras
|
|
- Cards that care about equipped/enchanted creatures
|
|
- Cards that enhance single creatures
|
|
|
|
The function uses vectorized operations for performance and follows patterns
|
|
established in other tagging functions.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting Voltron strategy tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'type', 'name'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different Voltron aspects
|
|
commander_mask = create_voltron_commander_mask(df)
|
|
support_mask = create_voltron_support_mask(df)
|
|
equipment_mask = create_voltron_equipment_mask(df)
|
|
aura_mask = create_voltron_aura_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = commander_mask | support_mask | equipment_mask | aura_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Voltron'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with Voltron strategy in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_voltron: {str(e)}')
|
|
raise
|
|
duration = pd.Timestamp.now() - start_time
|
|
logger.info(f'Completed all "Life Matters" tagging in {duration.total_seconds():.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_voltron: {str(e)}')
|
|
raise
|
|
|
|
### Lands matter
|
|
def create_lands_matter_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that care about lands in general.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
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)
|
|
|
|
# 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'])
|
|
|
|
# Combine all masks
|
|
return name_mask | play_mask | search_mask | state_mask
|
|
|
|
def create_domain_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with domain effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
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'])
|
|
return keyword_mask | text_mask
|
|
|
|
def create_landfall_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with landfall triggers.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
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'])
|
|
return keyword_mask | trigger_mask
|
|
|
|
def create_landwalk_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with landwalk abilities.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
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'])
|
|
return basic_mask | nonbasic_mask
|
|
|
|
def create_land_types_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that care about specific land types.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
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)
|
|
|
|
# Create text pattern masks for each land type
|
|
text_masks = []
|
|
for land_type in settings.LAND_TYPES:
|
|
patterns = [
|
|
f'search your library for a {land_type.lower()}',
|
|
f'search your library for up to two {land_type.lower()}',
|
|
f'{land_type} you control'
|
|
]
|
|
text_masks.append(tag_utils.create_text_mask(df, patterns))
|
|
|
|
# Combine all masks
|
|
return type_mask | pd.concat(text_masks, axis=1).any(axis=1)
|
|
|
|
def tag_for_lands_matter(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about lands using vectorized operations.
|
|
|
|
This function identifies and tags cards with land-related effects including:
|
|
- General lands matter effects (searching, playing additional lands, etc)
|
|
- Domain effects
|
|
- Landfall triggers
|
|
- Landwalk abilities
|
|
- Specific land type matters
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting lands matter tagging for {color}_cards.csv')
|
|
print('\n==========\n')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'type', 'name'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different land effects
|
|
lands_mask = create_lands_matter_mask(df)
|
|
domain_mask = create_domain_mask(df)
|
|
landfall_mask = create_landfall_mask(df)
|
|
landwalk_mask = create_landwalk_mask(df)
|
|
types_mask = create_land_types_mask(df)
|
|
|
|
# Apply tags based on masks
|
|
if lands_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, lands_mask, ['Lands Matter'])
|
|
logger.info(f'Tagged {lands_mask.sum()} cards with general lands matter effects')
|
|
|
|
if domain_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, domain_mask, ['Domain', 'Lands Matter'])
|
|
logger.info(f'Tagged {domain_mask.sum()} cards with domain effects')
|
|
|
|
if landfall_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, landfall_mask, ['Landfall', 'Lands Matter'])
|
|
logger.info(f'Tagged {landfall_mask.sum()} cards with landfall effects')
|
|
|
|
if landwalk_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, landwalk_mask, ['Landwalk', 'Lands Matter'])
|
|
logger.info(f'Tagged {landwalk_mask.sum()} cards with landwalk abilities')
|
|
|
|
if types_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, types_mask, ['Land Types Matter', 'Lands Matter'])
|
|
logger.info(f'Tagged {types_mask.sum()} cards with specific land type effects')
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Completed lands matter tagging in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_lands_matter: {str(e)}')
|
|
raise
|
|
|
|
### Spells Matter
|
|
def create_spellslinger_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with spellslinger text patterns.
|
|
|
|
This function identifies cards that care about casting spells through text patterns like:
|
|
- Casting modal spells
|
|
- Casting spells from anywhere
|
|
- Casting instant/sorcery spells
|
|
- Casting noncreature spells
|
|
- First/next spell cast triggers
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have spellslinger text patterns
|
|
"""
|
|
text_patterns = [
|
|
'cast a modal',
|
|
'cast a spell from anywhere',
|
|
'cast an instant',
|
|
'cast a noncreature',
|
|
'casts an instant',
|
|
'casts a noncreature',
|
|
'first instant',
|
|
'first spell',
|
|
'next cast an instant',
|
|
'next instant',
|
|
'next spell',
|
|
'second instant',
|
|
'second spell',
|
|
'you cast an instant',
|
|
'you cast a spell'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_spellslinger_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with spellslinger-related keywords.
|
|
|
|
This function identifies cards with keywords that indicate they care about casting spells:
|
|
- Magecraft
|
|
- Storm
|
|
- Prowess
|
|
- Surge
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have spellslinger keywords
|
|
"""
|
|
keyword_patterns = [
|
|
'Magecraft',
|
|
'Storm',
|
|
'Prowess',
|
|
'Surge'
|
|
]
|
|
return tag_utils.create_keyword_mask(df, keyword_patterns)
|
|
|
|
def create_spellslinger_type_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for instant/sorcery type cards.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are instants or sorceries
|
|
"""
|
|
return tag_utils.create_type_mask(df, ['Instant', 'Sorcery'])
|
|
|
|
def create_spellslinger_exclusion_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that should be excluded from spellslinger tagging.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards should be excluded
|
|
"""
|
|
# Add specific exclusion patterns here if needed
|
|
excluded_names = [
|
|
'Possibility Storm',
|
|
'Wild-Magic Sorcerer'
|
|
]
|
|
return tag_utils.create_name_mask(df, excluded_names)
|
|
|
|
def tag_for_spellslinger(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about casting spells using vectorized operations.
|
|
|
|
This function identifies and tags cards that care about spellcasting including:
|
|
- Cards that trigger off casting spells
|
|
- Instant and sorcery spells
|
|
- Cards with spellslinger-related keywords
|
|
- Cards that care about noncreature spells
|
|
|
|
The function maintains proper tag hierarchy and ensures consistent application
|
|
of related tags like 'Spellslinger', 'Spells Matter', etc.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting Spellslinger tagging for {color}_cards.csv')
|
|
print('\n==========\n')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'type', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different spellslinger patterns
|
|
text_mask = create_spellslinger_text_mask(df)
|
|
keyword_mask = create_spellslinger_keyword_mask(df)
|
|
type_mask = create_spellslinger_type_mask(df)
|
|
exclusion_mask = create_spellslinger_exclusion_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = (text_mask | keyword_mask | type_mask) & ~exclusion_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Spellslinger', 'Spells Matter'])
|
|
logger.info(f'Tagged {final_mask.sum()} general Spellslinger cards')
|
|
|
|
# Run non-generalized tags
|
|
tag_for_storm(df, color)
|
|
tag_for_magecraft(df, color)
|
|
tag_for_cantrips(df, color)
|
|
tag_for_spell_copy(df, color)
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Completed Spellslinger tagging in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_spellslinger: {str(e)}')
|
|
raise
|
|
|
|
def create_storm_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with storm effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have storm effects
|
|
"""
|
|
# Create keyword mask
|
|
keyword_mask = tag_utils.create_keyword_mask(df, 'Storm')
|
|
|
|
# Create text mask
|
|
text_patterns = [
|
|
'gain storm',
|
|
'has storm',
|
|
'have storm'
|
|
]
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
return keyword_mask | text_mask
|
|
|
|
def tag_for_storm(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with storm effects using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Have the storm keyword
|
|
- Grant or care about storm
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create storm mask
|
|
storm_mask = create_storm_mask(df)
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, storm_mask, ['Storm', 'Spellslinger', 'Spells Matter'])
|
|
|
|
# Log results
|
|
storm_count = storm_mask.sum()
|
|
logger.info(f'Tagged {storm_count} cards with Storm effects')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Storm effects: {str(e)}')
|
|
raise
|
|
|
|
## Tag for Cantrips
|
|
def tag_for_cantrips(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards in the DataFrame as cantrips based on specific criteria.
|
|
|
|
Cantrips are defined as low-cost spells (mana value <= 2) that draw cards.
|
|
The function excludes certain card types, keywords, and specific named cards
|
|
from being tagged as cantrips.
|
|
|
|
Args:
|
|
df: The DataFrame containing card data
|
|
color: The color identifier for logging purposes
|
|
"""
|
|
try:
|
|
# Convert mana value to numeric
|
|
df['manaValue'] = pd.to_numeric(df['manaValue'], errors='coerce')
|
|
|
|
# Create exclusion masks
|
|
excluded_types = tag_utils.create_type_mask(df, 'Land|Equipment')
|
|
excluded_keywords = tag_utils.create_keyword_mask(df, ['Channel', 'Cycling', 'Connive', 'Learn', 'Ravenous'])
|
|
has_loot = df['themeTags'].apply(lambda x: 'Loot' in x)
|
|
|
|
# Define name exclusions
|
|
EXCLUDED_NAMES = {
|
|
'Archivist of Oghma', 'Argothian Enchantress', 'Audacity', 'Betrayal', 'Bequeathal', 'Blood Scrivener', 'Brigon, Soldier of Meletis',
|
|
'Compost', 'Concealing curtains // Revealing Eye', 'Cryptbreaker', 'Curiosity', 'Cuse of Vengeance', 'Cryptek', 'Dakra Mystic',
|
|
'Dawn of a New Age', 'Dockside Chef', 'Dreamcatcher', 'Edgewall Innkeeper', 'Eidolon of Philosophy', 'Evolved Sleeper',
|
|
'Femeref Enchantress', 'Finneas, Ace Archer', 'Flumph', 'Folk Hero', 'Frodo, Adventurous Hobbit', 'Goblin Artisans',
|
|
'Goldberry, River-Daughter', 'Gollum, Scheming Guide', 'Hatching Plans', 'Ideas Unbound', 'Ingenius Prodigy', 'Ior Ruin Expedition',
|
|
"Jace's Erasure", 'Keeper of the Mind', 'Kor Spiritdancer', 'Lodestone Bauble', 'Puresteel Paladin', 'Jeweled Bird', 'Mindblade Render',
|
|
"Multani's Presence", "Nahiri's Lithoforming", 'Ordeal of Thassa', 'Pollywog Prodigy', 'Priest of Forgotten Gods', 'Ravenous Squirrel',
|
|
'Read the Runes', 'Red Death, Shipwrecker', 'Roil Cartographer', 'Sage of Lat-Name', 'Saprazzan Heir', 'Scion of Halaster', 'See Beyond',
|
|
'Selhoff Entomber', 'Shielded Aether Theif', 'Shore Keeper', 'silverquill Silencer', 'Soldevi Sage', 'Soldevi Sentry', 'Spiritual Focus',
|
|
'Sram, Senior Edificer', 'Staff of the Storyteller', 'Stirge', 'Sylvan Echoes', "Sythis Harvest's Hand", 'Sygg, River Cutthroat',
|
|
'Tenuous Truce', 'Test of Talents', 'Thalakos seer', "Tribute to Horobi // Echo of Deaths Wail", 'Vampire Gourmand', 'Vampiric Rites',
|
|
'Vampirism', 'Vessel of Paramnesia', "Witch's Caultron", 'Wall of Mulch', 'Waste Not', 'Well Rested'
|
|
# Add other excluded names here
|
|
}
|
|
excluded_names = df['name'].isin(EXCLUDED_NAMES)
|
|
|
|
# Create cantrip condition masks
|
|
has_draw = tag_utils.create_text_mask(df, PATTERN_GROUPS['draw'])
|
|
low_cost = df['manaValue'].fillna(float('inf')) <= 2
|
|
|
|
# Combine conditions
|
|
cantrip_mask = (
|
|
~excluded_types &
|
|
~excluded_keywords &
|
|
~has_loot &
|
|
~excluded_names &
|
|
has_draw &
|
|
low_cost
|
|
)
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, cantrip_mask, TAG_GROUPS['Cantrips'])
|
|
|
|
# Log results
|
|
cantrip_count = cantrip_mask.sum()
|
|
logger.info(f'Tagged {cantrip_count} Cantrip cards')
|
|
|
|
except Exception as e:
|
|
logger.error('Error tagging Cantrips in %s_cards.csv: %s', color, str(e))
|
|
raise
|
|
|
|
## Magecraft
|
|
def create_magecraft_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with magecraft effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have magecraft effects
|
|
"""
|
|
return tag_utils.create_keyword_mask(df, 'Magecraft')
|
|
|
|
def tag_for_magecraft(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards with magecraft using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'themeTags', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create magecraft mask
|
|
magecraft_mask = create_magecraft_mask(df)
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, magecraft_mask, ['Magecraft', 'Spellslinger', 'Spells Matter'])
|
|
|
|
# Log results
|
|
magecraft_count = magecraft_mask.sum()
|
|
logger.info(f'Tagged {magecraft_count} cards with Magecraft effects')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error tagging Magecraft effects: {str(e)}')
|
|
raise
|
|
|
|
## Spell Copy
|
|
def create_spell_copy_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with spell copy text patterns.
|
|
|
|
This function identifies cards that copy spells through text patterns like:
|
|
- Copy target spell
|
|
- Copy that spell
|
|
- Copy the next spell
|
|
- Create copies of spells
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have spell copy text patterns
|
|
"""
|
|
text_patterns = [
|
|
'copy a spell',
|
|
'copy it',
|
|
'copy that spell',
|
|
'copy target',
|
|
'copy the next',
|
|
'create a copy',
|
|
'creates a copy'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_spell_copy_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with spell copy related keywords.
|
|
|
|
This function identifies cards with keywords that indicate they copy spells:
|
|
- Casualty
|
|
- Conspire
|
|
- Replicate
|
|
- Storm
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have spell copy keywords
|
|
"""
|
|
keyword_patterns = [
|
|
'Casualty',
|
|
'Conspire',
|
|
'Replicate',
|
|
'Storm'
|
|
]
|
|
return tag_utils.create_keyword_mask(df, keyword_patterns)
|
|
|
|
def tag_for_spell_copy(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that copy spells using vectorized operations.
|
|
|
|
This function identifies and tags cards that copy spells including:
|
|
- Cards that directly copy spells
|
|
- Cards with copy-related keywords
|
|
- Cards that create copies of spells
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different spell copy patterns
|
|
text_mask = create_spell_copy_text_mask(df)
|
|
keyword_mask = create_spell_copy_keyword_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | keyword_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Spell Copy', 'Spellslinger', 'Spells Matter'])
|
|
|
|
# Log results
|
|
spellcopy_count = final_mask.sum()
|
|
logger.info(f'Tagged {spellcopy_count} spell copy cards')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_spell_copy: {str(e)}')
|
|
raise
|
|
|
|
### Ramp
|
|
def create_mana_dork_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for creatures that produce mana.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are mana dorks
|
|
"""
|
|
# Create base creature mask
|
|
creature_mask = tag_utils.create_type_mask(df, 'Creature')
|
|
|
|
# Create text pattern masks
|
|
tap_mask = tag_utils.create_text_mask(df, ['{T}: Add', '{T}: Untap'])
|
|
sac_mask = tag_utils.create_text_mask(df, ['creature: add', 'control: add'])
|
|
|
|
# Create mana symbol mask
|
|
mana_patterns = [f'add {{{c}}}' for c in ['C', 'W', 'U', 'B', 'R', 'G']]
|
|
mana_mask = tag_utils.create_text_mask(df, mana_patterns)
|
|
|
|
# Create specific cards mask
|
|
specific_cards = ['Awaken the Woods', 'Forest Dryad']
|
|
name_mask = tag_utils.create_name_mask(df, specific_cards)
|
|
|
|
return creature_mask & (tap_mask | sac_mask | mana_mask) | name_mask
|
|
|
|
def create_mana_rock_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for artifacts that produce mana.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are mana rocks
|
|
"""
|
|
# Create base artifact mask
|
|
artifact_mask = tag_utils.create_type_mask(df, 'Artifact')
|
|
|
|
# Create text pattern masks
|
|
tap_mask = tag_utils.create_text_mask(df, ['{T}: Add', '{T}: Untap'])
|
|
sac_mask = tag_utils.create_text_mask(df, ['creature: add', 'control: add'])
|
|
|
|
# Create mana symbol mask
|
|
mana_patterns = [f'add {{{c}}}' for c in ['C', 'W', 'U', 'B', 'R', 'G']]
|
|
mana_mask = tag_utils.create_text_mask(df, mana_patterns)
|
|
|
|
# Create token mask
|
|
token_mask = tag_utils.create_tag_mask(df, ['Powerstone Tokens', 'Treasure Tokens', 'Gold Tokens']) | \
|
|
tag_utils.create_text_mask(df, 'token named meteorite')
|
|
|
|
return (artifact_mask & (tap_mask | sac_mask | mana_mask)) | token_mask
|
|
|
|
def create_extra_lands_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that allow playing additional lands.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards allow playing extra lands
|
|
"""
|
|
text_patterns = [
|
|
'additional land',
|
|
'play an additional land',
|
|
'play two additional lands',
|
|
'put a land',
|
|
'put all land',
|
|
'put those land',
|
|
'return all land',
|
|
'return target land'
|
|
]
|
|
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_land_search_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that search for lands.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards search for lands
|
|
"""
|
|
# Create basic search patterns
|
|
search_patterns = [
|
|
'search your library for a basic',
|
|
'search your library for a land',
|
|
'search your library for up to',
|
|
'each player searches',
|
|
'put those land'
|
|
]
|
|
|
|
# Create land type specific patterns
|
|
land_types = ['Plains', 'Island', 'Swamp', 'Mountain', 'Forest', 'Wastes']
|
|
for land_type in land_types:
|
|
search_patterns.extend([
|
|
f'search your library for a basic {land_type.lower()}',
|
|
f'search your library for a {land_type.lower()}',
|
|
f'search your library for an {land_type.lower()}'
|
|
])
|
|
|
|
return tag_utils.create_text_mask(df, search_patterns)
|
|
|
|
def tag_for_ramp(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that provide mana acceleration using vectorized operations.
|
|
|
|
This function identifies and tags cards that provide mana acceleration through:
|
|
- Mana dorks (creatures that produce mana)
|
|
- Mana rocks (artifacts that produce mana)
|
|
- Extra land effects
|
|
- Land search effects
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting ramp tagging for {color}_cards.csv')
|
|
print('\n==========\n')
|
|
|
|
try:
|
|
# Create masks for different ramp categories
|
|
dork_mask = create_mana_dork_mask(df)
|
|
rock_mask = create_mana_rock_mask(df)
|
|
lands_mask = create_extra_lands_mask(df)
|
|
search_mask = create_land_search_mask(df)
|
|
|
|
# Apply tags for each category
|
|
if dork_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, dork_mask, ['Mana Dork', 'Ramp'])
|
|
logger.info(f'Tagged {dork_mask.sum()} mana dork cards')
|
|
|
|
if rock_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, rock_mask, ['Mana Rock', 'Ramp'])
|
|
logger.info(f'Tagged {rock_mask.sum()} mana rock cards')
|
|
|
|
if lands_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, lands_mask, ['Lands Matter', 'Ramp'])
|
|
logger.info(f'Tagged {lands_mask.sum()} extra lands cards')
|
|
|
|
if search_mask.any():
|
|
tag_utils.apply_tag_vectorized(df, search_mask, ['Lands Matter', 'Ramp'])
|
|
logger.info(f'Tagged {search_mask.sum()} land search cards')
|
|
|
|
# Log completion
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Completed ramp tagging in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_ramp: {str(e)}')
|
|
raise
|
|
|
|
### Other Misc Themes
|
|
def tag_for_themes(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that fit other themes that haven't been done so far.
|
|
|
|
This function will call on functions to tag for:
|
|
- Aggo
|
|
- Aristocrats
|
|
- Big Mana
|
|
- Blink
|
|
- Burn
|
|
- Clones
|
|
- Control
|
|
- Energy
|
|
- Infect
|
|
- Legends Matter
|
|
- Little Creatures
|
|
- Mill
|
|
- Monarch
|
|
- Multiple Copy Cards (i.e. Hare Apparent or Dragon's Approach)
|
|
- Superfriends
|
|
- Reanimate
|
|
- Stax
|
|
- Theft
|
|
- Toughess Matters
|
|
- Topdeck
|
|
- X Spells
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting tagging for remaining themes in {color}_cards.csv')
|
|
print('\n===============\n')
|
|
tag_for_aggro(df, color)
|
|
print('\n==========\n')
|
|
tag_for_aristocrats(df, color)
|
|
print('\n==========\n')
|
|
tag_for_big_mana(df, color)
|
|
print('\n==========\n')
|
|
tag_for_blink(df, color)
|
|
print('\n==========\n')
|
|
tag_for_burn(df, color)
|
|
print('\n==========\n')
|
|
tag_for_clones(df, color)
|
|
print('\n==========\n')
|
|
tag_for_control(df, color)
|
|
print('\n==========\n')
|
|
tag_for_energy(df, color)
|
|
print('\n==========\n')
|
|
tag_for_infect(df, color)
|
|
print('\n==========\n')
|
|
tag_for_legends_matter(df, color)
|
|
print('\n==========\n')
|
|
tag_for_little_guys(df, color)
|
|
print('\n==========\n')
|
|
tag_for_mill(df, color)
|
|
print('\n==========\n')
|
|
tag_for_monarch(df, color)
|
|
print('\n==========\n')
|
|
tag_for_multiple_copies(df, color)
|
|
print('\n==========\n')
|
|
tag_for_planeswalkers(df, color)
|
|
print('\n==========\n')
|
|
tag_for_reanimate(df, color)
|
|
print('\n==========\n')
|
|
tag_for_stax(df, color)
|
|
print('\n==========\n')
|
|
tag_for_theft(df, color)
|
|
print('\n==========\n')
|
|
tag_for_toughness(df, color)
|
|
print('\n==========\n')
|
|
tag_for_topdeck(df, color)
|
|
print('\n==========\n')
|
|
tag_for_x_spells(df, color)
|
|
print('\n==========\n')
|
|
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Completed theme tagging in {duration:.2f}s')
|
|
|
|
## Aggro
|
|
def create_aggro_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with aggro-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have aggro text patterns
|
|
"""
|
|
text_patterns = [
|
|
'a creature attacking',
|
|
'deal combat damage',
|
|
'deals combat damage',
|
|
'have riot',
|
|
'this creature attacks',
|
|
'whenever you attack',
|
|
'whenever .* attack',
|
|
'whenever .* deals combat',
|
|
'you control attack',
|
|
'you control deals combat',
|
|
'untap all attacking creatures'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_aggro_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with aggro-related keywords.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have aggro keywords
|
|
"""
|
|
keyword_patterns = [
|
|
'Blitz',
|
|
'Deathtouch',
|
|
'Double Strike',
|
|
'First Strike',
|
|
'Fear',
|
|
'Haste',
|
|
'Menace',
|
|
'Myriad',
|
|
'Prowl',
|
|
'Raid',
|
|
'Shadow',
|
|
'Spectacle',
|
|
'Trample'
|
|
]
|
|
return tag_utils.create_keyword_mask(df, keyword_patterns)
|
|
|
|
def create_aggro_theme_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with aggro-related themes.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have aggro themes
|
|
"""
|
|
return tag_utils.create_tag_mask(df, ['Voltron'])
|
|
|
|
def tag_for_aggro(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that fit the Aggro theme using vectorized operations.
|
|
|
|
This function identifies and tags cards that support aggressive strategies including:
|
|
- Cards that care about attacking
|
|
- Cards with combat-related keywords
|
|
- Cards that deal combat damage
|
|
- Cards that support Voltron strategies
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting Aggro strategy tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different aggro aspects
|
|
text_mask = create_aggro_text_mask(df)
|
|
keyword_mask = create_aggro_keyword_mask(df)
|
|
theme_mask = create_aggro_theme_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | keyword_mask | theme_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Aggro', 'Combat Matters'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with Aggro strategy in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_aggro: {str(e)}')
|
|
raise
|
|
|
|
|
|
## Aristocrats
|
|
def create_aristocrat_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with aristocrat-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have aristocrat text patterns
|
|
"""
|
|
return tag_utils.create_text_mask(df, settings.ARISTOCRAT_TEXT_PATTERNS)
|
|
|
|
def create_aristocrat_name_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for specific aristocrat-related cards.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are specific aristocrat cards
|
|
"""
|
|
return tag_utils.create_name_mask(df, settings.ARISTOCRAT_SPECIFIC_CARDS)
|
|
|
|
def create_aristocrat_self_sacrifice_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for creatures with self-sacrifice effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which creatures have self-sacrifice effects
|
|
"""
|
|
# Create base creature mask
|
|
creature_mask = tag_utils.create_type_mask(df, 'Creature')
|
|
|
|
# Create name-based patterns
|
|
def check_self_sacrifice(row):
|
|
if pd.isna(row['text']) or pd.isna(row['name']):
|
|
return False
|
|
name = row['name'].lower()
|
|
text = row['text'].lower()
|
|
return f'sacrifice {name}' in text or f'when {name} dies' in text
|
|
|
|
# Apply patterns to creature cards
|
|
return creature_mask & df.apply(check_self_sacrifice, axis=1)
|
|
|
|
def create_aristocrat_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with aristocrat-related keywords.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have aristocrat keywords
|
|
"""
|
|
return tag_utils.create_keyword_mask(df, 'Blitz')
|
|
|
|
def create_aristocrat_exclusion_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that should be excluded from aristocrat effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards should be excluded
|
|
"""
|
|
return tag_utils.create_text_mask(df, settings.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.
|
|
|
|
This function identifies and tags cards that care about sacrificing permanents or creatures dying, including:
|
|
- Cards with sacrifice abilities or triggers
|
|
- Cards that care about creatures dying
|
|
- Cards with self-sacrifice effects
|
|
- Cards with Blitz or similar mechanics
|
|
|
|
The function uses efficient vectorized operations and separate mask creation functions
|
|
for different aspects of the aristocrats theme. It handles:
|
|
- Text-based patterns for sacrifice and death triggers
|
|
- Specific named cards known for aristocrats strategies
|
|
- Self-sacrifice effects on creatures
|
|
- Relevant keywords like Blitz
|
|
- Proper exclusions to avoid false positives
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting aristocrats effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'name', 'type', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different aristocrat patterns
|
|
text_mask = create_aristocrat_text_mask(df)
|
|
name_mask = create_aristocrat_name_mask(df)
|
|
self_sacrifice_mask = create_aristocrat_self_sacrifice_mask(df)
|
|
keyword_mask = create_aristocrat_keyword_mask(df)
|
|
exclusion_mask = create_aristocrat_exclusion_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = (text_mask | name_mask | self_sacrifice_mask | keyword_mask) & ~exclusion_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Aristocrats', 'Sacrifice Matters'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with aristocrats effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_aristocrats: {str(e)}')
|
|
raise
|
|
|
|
|
|
## Big Mana
|
|
def create_big_mana_cost_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with high mana costs or X costs.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have high/X mana costs
|
|
"""
|
|
# High mana value mask
|
|
high_cost = df['manaValue'].fillna(0).astype(float) >= 5
|
|
|
|
# X cost mask
|
|
x_cost = df['manaCost'].fillna('').str.contains('{X}', case=False, regex=False)
|
|
|
|
return high_cost | x_cost
|
|
|
|
def tag_for_big_mana(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about or generate large amounts of mana using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Have high mana costs (5 or greater)
|
|
- Care about high mana values or power
|
|
- Generate large amounts of mana
|
|
- Have X costs
|
|
- Have keywords related to mana generation
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting big mana tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'manaValue', 'manaCost', 'keywords'}
|
|
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)
|
|
cost_mask = create_big_mana_cost_mask(df)
|
|
specific_mask = tag_utils.create_name_mask(df, settings.BIG_MANA_SPECIFIC_CARDS)
|
|
tag_mask = tag_utils.create_tag_mask(df, 'Cost Reduction')
|
|
|
|
# Combine all masks
|
|
final_mask = text_mask | keyword_mask | cost_mask | specific_mask | tag_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Big Mana'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with big mana effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_big_mana: {str(e)}')
|
|
raise
|
|
|
|
## Blink
|
|
def create_etb_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with enter-the-battlefield effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have ETB effects
|
|
"""
|
|
text_patterns = [
|
|
'creature entering causes',
|
|
'permanent entering the battlefield',
|
|
'permanent you control enters',
|
|
'whenever another creature enters',
|
|
'whenever another nontoken creature enters',
|
|
'when this creature enters',
|
|
'whenever this creature enters'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_ltb_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with leave-the-battlefield effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have LTB effects
|
|
"""
|
|
text_patterns = [
|
|
'when this creature leaves',
|
|
'whenever this creature leaves'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_blink_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with blink/flicker text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have blink/flicker effects
|
|
"""
|
|
text_patterns = [
|
|
'exile any number of other',
|
|
'exile one or more cards from your hand',
|
|
'permanent you control, then return',
|
|
'permanents you control, then return',
|
|
'return it to the battlefield',
|
|
'return that card to the battlefield',
|
|
'return them to the battlefield',
|
|
'return those cards to the battlefield',
|
|
'triggered ability of a permanent'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def tag_for_blink(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that have blink/flicker effects using vectorized operations.
|
|
|
|
This function identifies and tags cards with blink/flicker effects including:
|
|
- Enter-the-battlefield (ETB) triggers
|
|
- Leave-the-battlefield (LTB) triggers
|
|
- Exile and return effects
|
|
- Permanent flicker effects
|
|
|
|
The function maintains proper tag hierarchy and ensures consistent application
|
|
of related tags like 'Blink', 'Enter the Battlefield', and 'Leave the Battlefield'.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting blink/flicker effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'name'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different blink patterns
|
|
etb_mask = create_etb_mask(df)
|
|
ltb_mask = create_ltb_mask(df)
|
|
blink_mask = create_blink_text_mask(df)
|
|
|
|
# Create name-based masks
|
|
name_patterns = df.apply(
|
|
lambda row: f'when {row["name"]} enters|whenever {row["name"]} enters|when {row["name"]} leaves|whenever {row["name"]} leaves',
|
|
axis=1
|
|
)
|
|
name_mask = df.apply(
|
|
lambda row: bool(re.search(name_patterns[row.name], row['text'], re.IGNORECASE)) if pd.notna(row['text']) else False,
|
|
axis=1
|
|
)
|
|
|
|
# Combine all masks
|
|
final_mask = etb_mask | ltb_mask | blink_mask | name_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Blink', 'Enter the Battlefield', 'Leave the Battlefield'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with blink/flicker effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_blink: {str(e)}')
|
|
raise
|
|
|
|
## Burn
|
|
def create_burn_damage_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with damage-dealing effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have damage effects
|
|
"""
|
|
# Create damage number patterns using list comprehension
|
|
damage_patterns = [f'deals {i} damage' for i in range(1, 101)] + ['deals x damage']
|
|
damage_mask = tag_utils.create_text_mask(df, damage_patterns)
|
|
|
|
# Create general damage trigger patterns
|
|
trigger_patterns = [
|
|
'deals combat damage',
|
|
'deals damage',
|
|
'deals noncombat damage',
|
|
'deals that much damage',
|
|
'excess damage',
|
|
'excess noncombat damage',
|
|
'would deal an amount of noncombat damage',
|
|
'would deal damage',
|
|
'would deal noncombat damage'
|
|
]
|
|
trigger_mask = tag_utils.create_text_mask(df, trigger_patterns)
|
|
|
|
# Create pinger patterns
|
|
pinger_patterns = ['deals 1 damage', 'exactly 1 damage']
|
|
pinger_mask = tag_utils.create_text_mask(df, pinger_patterns)
|
|
|
|
return damage_mask | trigger_mask | pinger_mask
|
|
|
|
def create_burn_life_loss_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with life loss effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have life loss effects
|
|
"""
|
|
# Create life loss number patterns
|
|
life_patterns = [f'lose {i} life' for i in range(1, 101)]
|
|
life_patterns.extend([f'loses {i} life' for i in range(1, 101)])
|
|
life_patterns.append('lose x life')
|
|
life_patterns.append('loses x life')
|
|
life_mask = tag_utils.create_text_mask(df, life_patterns)
|
|
|
|
# Create general life loss trigger patterns
|
|
trigger_patterns = [
|
|
'each 1 life',
|
|
'loses that much life',
|
|
'opponent lost life',
|
|
'opponent loses life',
|
|
'player loses life',
|
|
'unspent mana causes that player to lose that much life',
|
|
'would lose life'
|
|
]
|
|
trigger_mask = tag_utils.create_text_mask(df, trigger_patterns)
|
|
|
|
return life_mask | trigger_mask
|
|
|
|
def create_burn_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with burn-related keywords.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have burn keywords
|
|
"""
|
|
keyword_patterns = ['Bloodthirst', 'Spectacle']
|
|
return tag_utils.create_keyword_mask(df, keyword_patterns)
|
|
|
|
def create_burn_exclusion_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that should be excluded from burn effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards should be excluded
|
|
"""
|
|
# Add specific exclusion patterns here if needed
|
|
return pd.Series(False, index=df.index)
|
|
|
|
def tag_for_burn(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that deal damage or cause life loss using vectorized operations.
|
|
|
|
This function identifies and tags cards with burn effects including:
|
|
- Direct damage dealing
|
|
- Life loss effects
|
|
- Burn-related keywords (Bloodthirst, Spectacle)
|
|
- Pinger effects (1 damage)
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting burn effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different burn patterns
|
|
damage_mask = create_burn_damage_mask(df)
|
|
life_mask = create_burn_life_loss_mask(df)
|
|
keyword_mask = create_burn_keyword_mask(df)
|
|
exclusion_mask = create_burn_exclusion_mask(df)
|
|
|
|
# Combine masks
|
|
burn_mask = (damage_mask | life_mask | keyword_mask) & ~exclusion_mask
|
|
pinger_mask = tag_utils.create_text_mask(df, ['deals 1 damage', 'exactly 1 damage', 'loses 1 life'])
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, burn_mask, ['Burn'])
|
|
tag_utils.apply_tag_vectorized(df, pinger_mask & ~exclusion_mask, ['Pingers'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {burn_mask.sum()} cards with burn effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_burn: {str(e)}')
|
|
raise
|
|
|
|
## Clones
|
|
def create_clone_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with clone-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have clone text patterns
|
|
"""
|
|
text_patterns = [
|
|
'a copy of a creature',
|
|
'a copy of an aura',
|
|
'a copy of a permanent',
|
|
'a token that\'s a copy of',
|
|
'as a copy of',
|
|
'becomes a copy of',
|
|
'"legend rule" doesn\'t apply',
|
|
'twice that many of those tokens'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_clone_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with clone-related keywords.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have clone keywords
|
|
"""
|
|
return tag_utils.create_keyword_mask(df, 'Myriad')
|
|
|
|
def create_clone_exclusion_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that should be excluded from clone effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards should be excluded
|
|
"""
|
|
# Add specific exclusion patterns here if needed
|
|
return pd.Series(False, index=df.index)
|
|
|
|
def tag_for_clones(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that create copies or have clone effects using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Create copies of creatures or permanents
|
|
- Have copy-related keywords like Myriad
|
|
- Ignore the legend rule
|
|
- Double token creation
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting clone effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different clone patterns
|
|
text_mask = create_clone_text_mask(df)
|
|
keyword_mask = create_clone_keyword_mask(df)
|
|
exclusion_mask = create_clone_exclusion_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = (text_mask | keyword_mask) & ~exclusion_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Clones'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with clone effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_clones: {str(e)}')
|
|
raise
|
|
|
|
## Control
|
|
def create_control_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with control-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have control text patterns
|
|
"""
|
|
text_patterns = [
|
|
'a player casts',
|
|
'can\'t attack you',
|
|
'cast your first spell during each opponent\'s turn',
|
|
'choose new target',
|
|
'choose target opponent',
|
|
'counter target',
|
|
'of an opponent\'s choice',
|
|
'opponent cast',
|
|
'return target',
|
|
'tap an untapped creature',
|
|
'your opponents cast'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_control_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with control-related keywords.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have control keywords
|
|
"""
|
|
keyword_patterns = ['Council\'s dilemma']
|
|
return tag_utils.create_keyword_mask(df, keyword_patterns)
|
|
|
|
def create_control_specific_cards_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for specific control-related cards.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are specific control cards
|
|
"""
|
|
specific_cards = [
|
|
'Azor\'s Elocutors',
|
|
'Baral, Chief of Compliance',
|
|
'Dragonlord Ojutai',
|
|
'Grand Arbiter Augustin IV',
|
|
'Lavinia, Azorius Renegade',
|
|
'Talrand, Sky Summoner'
|
|
]
|
|
return tag_utils.create_name_mask(df, specific_cards)
|
|
|
|
def tag_for_control(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that fit the Control theme using vectorized operations.
|
|
|
|
This function identifies and tags cards that control the game through:
|
|
- Counter magic
|
|
- Bounce effects
|
|
- Tap effects
|
|
- Opponent restrictions
|
|
- Council's dilemma effects
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting control effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords', 'name'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different control patterns
|
|
text_mask = create_control_text_mask(df)
|
|
keyword_mask = create_control_keyword_mask(df)
|
|
specific_mask = create_control_specific_cards_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | keyword_mask | specific_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Control'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with control effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_control: {str(e)}')
|
|
raise
|
|
|
|
## Energy
|
|
def tag_for_energy(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about energy counters using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Use energy counters ({E})
|
|
- Care about energy counters
|
|
- Generate or spend energy
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting energy counter tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create mask for energy text
|
|
energy_mask = df['text'].str.contains('{e}', case=False, na=False)
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, energy_mask, ['Energy'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {energy_mask.sum()} cards with energy effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_energy: {str(e)}')
|
|
raise
|
|
|
|
## Infect
|
|
def create_infect_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with infect-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have infect text patterns
|
|
"""
|
|
text_patterns = [
|
|
'one or more counter',
|
|
'poison counter',
|
|
'toxic [1-10]',
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_infect_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with infect-related keywords.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have infect keywords
|
|
"""
|
|
keyword_patterns = [
|
|
'Infect',
|
|
'Proliferate',
|
|
'Toxic',
|
|
]
|
|
return tag_utils.create_keyword_mask(df, keyword_patterns)
|
|
|
|
def create_infect_exclusion_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that should be excluded from infect effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards should be excluded
|
|
"""
|
|
# Add specific exclusion patterns here if needed
|
|
return pd.Series(False, index=df.index)
|
|
|
|
def tag_for_infect(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that have infect-related effects using vectorized operations.
|
|
|
|
This function identifies and tags cards with infect effects including:
|
|
- Infect keyword ability
|
|
- Toxic keyword ability
|
|
- Proliferate mechanic
|
|
- Poison counter effects
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting infect effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different infect patterns
|
|
text_mask = create_infect_text_mask(df)
|
|
keyword_mask = create_infect_keyword_mask(df)
|
|
exclusion_mask = create_infect_exclusion_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = (text_mask | keyword_mask) & ~exclusion_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Infect'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with infect effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_infect: {str(e)}')
|
|
raise
|
|
|
|
## Legends Matter
|
|
def create_legends_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with legendary/historic text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have legendary/historic text patterns
|
|
"""
|
|
text_patterns = [
|
|
'a legendary creature',
|
|
'another legendary',
|
|
'cast a historic',
|
|
'cast a legendary',
|
|
'cast legendary',
|
|
'equip legendary',
|
|
'historic cards',
|
|
'historic creature',
|
|
'historic permanent',
|
|
'historic spells',
|
|
'legendary creature you control',
|
|
'legendary creatures you control',
|
|
'legendary permanents',
|
|
'legendary spells you',
|
|
'number of legendary',
|
|
'other legendary',
|
|
'play a historic',
|
|
'play a legendary',
|
|
'target legendary',
|
|
'the "legend rule" doesn\'t'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_legends_type_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with Legendary in their type line.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are Legendary
|
|
"""
|
|
return tag_utils.create_type_mask(df, 'Legendary')
|
|
|
|
def tag_for_legends_matter(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about legendary permanents using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Are legendary permanents
|
|
- Care about legendary permanents
|
|
- Care about historic spells/permanents
|
|
- Modify the legend rule
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting legendary/historic tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'type'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different legendary patterns
|
|
text_mask = create_legends_text_mask(df)
|
|
type_mask = create_legends_type_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | type_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Historics Matter', 'Legends Matter'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with legendary/historic effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_legends_matter: {str(e)}')
|
|
raise
|
|
|
|
## Little Fellas
|
|
def create_little_guys_power_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for creatures with power 2 or less.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have power 2 or less
|
|
"""
|
|
# Create mask for valid power values
|
|
valid_power = pd.to_numeric(df['power'], errors='coerce')
|
|
|
|
# Create mask for power <= 2
|
|
return (valid_power <= 2) & pd.notna(valid_power)
|
|
|
|
def tag_for_little_guys(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that are or care about low-power creatures using vectorized operations.
|
|
|
|
This function identifies and tags:
|
|
- Creatures with power 2 or less
|
|
- Cards that care about creatures with low power
|
|
- Cards that reference power thresholds of 2 or less
|
|
|
|
The function handles edge cases like '*' in power values and maintains proper
|
|
tag hierarchy.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting low-power creature tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'power', 'text', 'themeTags'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different patterns
|
|
power_mask = create_little_guys_power_mask(df)
|
|
text_mask = tag_utils.create_text_mask(df, 'power 2 or less')
|
|
|
|
# Combine masks
|
|
final_mask = power_mask | text_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Little Fellas'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with Little Fellas in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_little_guys: {str(e)}')
|
|
raise
|
|
|
|
## Mill
|
|
def create_mill_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with mill-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have mill text patterns
|
|
"""
|
|
# Create text pattern masks
|
|
text_patterns = [
|
|
'descended',
|
|
'from a graveyard',
|
|
'from your graveyard',
|
|
'in your graveyard',
|
|
'into his or her graveyard',
|
|
'into their graveyard',
|
|
'into your graveyard',
|
|
'mills that many cards',
|
|
'opponent\'s graveyard',
|
|
'put into a graveyard',
|
|
'put into an opponent\'s graveyard',
|
|
'put into your graveyard',
|
|
'rad counter',
|
|
'surveil',
|
|
'would mill'
|
|
]
|
|
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])
|
|
number_mask = tag_utils.create_text_mask(df, mill_patterns)
|
|
|
|
return text_mask | number_mask
|
|
|
|
def create_mill_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with mill-related keywords.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have mill keywords
|
|
"""
|
|
keyword_patterns = ['Descend', 'Mill', 'Surveil']
|
|
return tag_utils.create_keyword_mask(df, keyword_patterns)
|
|
|
|
def tag_for_mill(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that mill cards or care about milling using vectorized operations.
|
|
|
|
This function identifies and tags cards with mill effects including:
|
|
- Direct mill effects (putting cards from library to graveyard)
|
|
- Mill-related keywords (Descend, Mill, Surveil)
|
|
- Cards that care about graveyards
|
|
- Cards that track milled cards
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting mill effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different mill patterns
|
|
text_mask = create_mill_text_mask(df)
|
|
keyword_mask = create_mill_keyword_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | keyword_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Mill'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with mill effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_mill: {str(e)}')
|
|
raise
|
|
|
|
def tag_for_monarch(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about the monarch mechanic using vectorized operations.
|
|
|
|
This function identifies and tags cards that interact with the monarch mechanic, including:
|
|
- Cards that make you become the monarch
|
|
- Cards that prevent becoming the monarch
|
|
- Cards with monarch-related triggers
|
|
- Cards with the monarch keyword
|
|
|
|
The function uses vectorized operations for performance and follows patterns
|
|
established in other tagging functions.
|
|
|
|
Args:
|
|
df: DataFrame containing card data with text and keyword columns
|
|
color: Color identifier for logging purposes (e.g. 'white', 'blue')
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting monarch mechanic tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create text pattern mask
|
|
text_patterns = [
|
|
'becomes? the monarch',
|
|
'can\'t become the monarch',
|
|
'is the monarch',
|
|
'was the monarch',
|
|
'you are the monarch',
|
|
'you become the monarch',
|
|
'you can\'t become the monarch',
|
|
'you\'re the monarch'
|
|
]
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
# Create keyword mask
|
|
keyword_mask = tag_utils.create_keyword_mask(df, 'Monarch')
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | keyword_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Monarch'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with monarch effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_monarch: {str(e)}')
|
|
raise
|
|
|
|
## Multi-copy cards
|
|
def tag_for_multiple_copies(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that allow having multiple copies in a deck using vectorized operations.
|
|
|
|
This function identifies and tags cards that can have more than 4 copies in a deck,
|
|
like Seven Dwarves or Persistent Petitioners. It uses the multiple_copy_cards list
|
|
from settings to identify these cards.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting multiple copies tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'name', 'themeTags'}
|
|
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)
|
|
|
|
# Apply tags
|
|
if multiple_copies_mask.any():
|
|
# Get matching card names
|
|
matching_cards = df[multiple_copies_mask]['name'].unique()
|
|
|
|
# Apply base tag
|
|
tag_utils.apply_tag_vectorized(df, multiple_copies_mask, ['Multiple Copies'])
|
|
|
|
# Apply individual card name tags
|
|
for card_name in matching_cards:
|
|
card_mask = df['name'] == card_name
|
|
tag_utils.apply_tag_vectorized(df, card_mask, [card_name])
|
|
|
|
logger.info(f'Tagged {multiple_copies_mask.sum()} cards with multiple copies effects')
|
|
|
|
# Log completion
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Completed multiple copies tagging in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_multiple_copies: {str(e)}')
|
|
raise
|
|
|
|
## Planeswalkers
|
|
def create_planeswalker_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with planeswalker-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have planeswalker text patterns
|
|
"""
|
|
text_patterns = [
|
|
'a planeswalker',
|
|
'affinity for planeswalker',
|
|
'enchant planeswalker',
|
|
'historic permanent',
|
|
'legendary permanent',
|
|
'loyalty ability',
|
|
'one or more counter',
|
|
'planeswalker spells',
|
|
'planeswalker type'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_planeswalker_type_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with Planeswalker type.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are Planeswalkers
|
|
"""
|
|
return tag_utils.create_type_mask(df, 'Planeswalker')
|
|
|
|
def create_planeswalker_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with planeswalker-related keywords.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have planeswalker keywords
|
|
"""
|
|
return tag_utils.create_keyword_mask(df, 'Proliferate')
|
|
|
|
def tag_for_planeswalkers(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about planeswalkers using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Are planeswalker cards
|
|
- Care about planeswalkers
|
|
- Have planeswalker-related keywords like Proliferate
|
|
- Interact with loyalty abilities
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting planeswalker tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'type', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different planeswalker patterns
|
|
text_mask = create_planeswalker_text_mask(df)
|
|
type_mask = create_planeswalker_type_mask(df)
|
|
keyword_mask = create_planeswalker_keyword_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | type_mask | keyword_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Planeswalkers', 'Super Friends'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with planeswalker effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_planeswalkers: {str(e)}')
|
|
raise
|
|
|
|
## Reanimator
|
|
def create_reanimator_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with reanimator-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have reanimator text patterns
|
|
"""
|
|
text_patterns = [
|
|
'descended',
|
|
'discard your hand',
|
|
'from a graveyard',
|
|
'in a graveyard',
|
|
'into a graveyard',
|
|
'leave a graveyard',
|
|
'in your graveyard',
|
|
'into your graveyard',
|
|
'leave your graveyard'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_reanimator_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with reanimator-related keywords.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have reanimator keywords
|
|
"""
|
|
keyword_patterns = [
|
|
'Blitz',
|
|
'Connive',
|
|
'Descend',
|
|
'Escape',
|
|
'Flashback',
|
|
'Mill'
|
|
]
|
|
return tag_utils.create_keyword_mask(df, keyword_patterns)
|
|
|
|
def create_reanimator_type_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with reanimator-related creature types.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have reanimator creature types
|
|
"""
|
|
return df['creatureTypes'].apply(lambda x: 'Zombie' in x if isinstance(x, list) else False)
|
|
|
|
def tag_for_reanimate(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about graveyard recursion using vectorized operations.
|
|
|
|
This function identifies and tags cards with reanimator effects including:
|
|
- Cards that interact with graveyards
|
|
- Cards with reanimator-related keywords (Blitz, Connive, etc)
|
|
- Cards that loot or mill
|
|
- Zombie tribal synergies
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting reanimator effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords', 'creatureTypes'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different reanimator patterns
|
|
text_mask = create_reanimator_text_mask(df)
|
|
keyword_mask = create_reanimator_keyword_mask(df)
|
|
type_mask = create_reanimator_type_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | keyword_mask | type_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Reanimate'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with reanimator effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_reanimate: {str(e)}')
|
|
raise
|
|
|
|
## Stax
|
|
def create_stax_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with stax-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have stax text patterns
|
|
"""
|
|
return tag_utils.create_text_mask(df, settings.STAX_TEXT_PATTERNS)
|
|
|
|
def create_stax_name_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards used in stax strategies.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have stax text patterns
|
|
"""
|
|
return tag_utils.create_text_mask(df, settings.STAX_SPECIFIC_CARDS)
|
|
|
|
def create_stax_tag_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with stax-related tags.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have stax tags
|
|
"""
|
|
return tag_utils.create_tag_mask(df, 'Control')
|
|
|
|
def create_stax_exclusion_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that should be excluded from stax effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
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)
|
|
|
|
def tag_for_stax(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that fit the Stax theme using vectorized operations.
|
|
|
|
This function identifies and tags cards that restrict or tax opponents including:
|
|
- Cards that prevent actions (can't attack, can't cast, etc)
|
|
- Cards that tax actions (spells cost more)
|
|
- Cards that control opponents' resources
|
|
- Cards that create asymmetric effects
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting stax effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different stax patterns
|
|
text_mask = create_stax_text_mask(df)
|
|
name_mask = create_stax_name_mask(df)
|
|
tag_mask = create_stax_tag_mask(df)
|
|
exclusion_mask = create_stax_exclusion_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = (text_mask | tag_mask | name_mask) & ~exclusion_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Stax'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with stax effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_stax: {str(e)}')
|
|
raise
|
|
|
|
## Theft
|
|
def create_theft_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with theft-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have theft text patterns
|
|
"""
|
|
return tag_utils.create_text_mask(df, settings.THEFT_TEXT_PATTERNS)
|
|
|
|
def create_theft_name_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for specific theft-related cards.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are specific theft cards
|
|
"""
|
|
return tag_utils.create_name_mask(df, settings.THEFT_SPECIFIC_CARDS)
|
|
|
|
def tag_for_theft(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that steal or use opponents' resources using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Cast spells owned by other players
|
|
- Take control of permanents
|
|
- Use opponents' libraries
|
|
- Create theft-related effects
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting theft effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'name'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different theft patterns
|
|
text_mask = create_theft_text_mask(df)
|
|
name_mask = create_theft_name_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | name_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Theft'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with theft effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_theft: {str(e)}')
|
|
raise
|
|
|
|
## Toughness Matters
|
|
def create_toughness_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with toughness-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have toughness text patterns
|
|
"""
|
|
text_patterns = [
|
|
'card\'s toughness',
|
|
'creature\'s toughness',
|
|
'damage equal to its toughness',
|
|
'lesser toughness',
|
|
'total toughness',
|
|
'toughness greater',
|
|
'with defender'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_toughness_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with toughness-related keywords.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have toughness keywords
|
|
"""
|
|
return tag_utils.create_keyword_mask(df, 'Defender')
|
|
|
|
def _is_valid_numeric_comparison(power: Union[int, str, None], toughness: Union[int, str, None]) -> bool:
|
|
"""Check if power and toughness values allow valid numeric comparison.
|
|
|
|
Args:
|
|
power: Power value to check
|
|
toughness: Toughness value to check
|
|
|
|
Returns:
|
|
True if values can be compared numerically, False otherwise
|
|
"""
|
|
try:
|
|
if power is None or toughness is None:
|
|
return False
|
|
return True
|
|
except (ValueError, TypeError):
|
|
return False
|
|
|
|
def create_power_toughness_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards where toughness exceeds power.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have toughness > power
|
|
"""
|
|
valid_comparison = df.apply(
|
|
lambda row: _is_valid_numeric_comparison(row['power'], row['toughness']),
|
|
axis=1
|
|
)
|
|
numeric_mask = valid_comparison & (pd.to_numeric(df['toughness'], errors='coerce') >
|
|
pd.to_numeric(df['power'], errors='coerce'))
|
|
return numeric_mask
|
|
|
|
def tag_for_toughness(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about toughness using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Reference toughness in their text
|
|
- Have the Defender keyword
|
|
- Have toughness greater than power
|
|
- Care about high toughness values
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting toughness tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords', 'power', 'toughness'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different toughness patterns
|
|
text_mask = create_toughness_text_mask(df)
|
|
keyword_mask = create_toughness_keyword_mask(df)
|
|
power_toughness_mask = create_power_toughness_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | keyword_mask | power_toughness_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Toughness Matters'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with toughness effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_toughness: {str(e)}')
|
|
raise
|
|
|
|
## Topdeck
|
|
def create_topdeck_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with topdeck-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have topdeck text patterns
|
|
"""
|
|
return tag_utils.create_text_mask(df, settings.TOPDECK_TEXT_PATTERNS)
|
|
|
|
def create_topdeck_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with topdeck-related keywords.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have topdeck keywords
|
|
"""
|
|
return tag_utils.create_keyword_mask(df, settings.TOPDECK_KEYWORDS)
|
|
|
|
def create_topdeck_specific_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for specific topdeck-related cards.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are specific topdeck cards
|
|
"""
|
|
return tag_utils.create_name_mask(df, settings.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.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards should be excluded
|
|
"""
|
|
return tag_utils.create_text_mask(df, settings.TOPDECK_EXCLUSION_PATTERNS)
|
|
|
|
def tag_for_topdeck(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that manipulate the top of library using vectorized operations.
|
|
|
|
This function identifies and tags cards that interact with the top of the library including:
|
|
- Cards that look at or reveal top cards
|
|
- Cards with scry or surveil effects
|
|
- Cards with miracle or similar mechanics
|
|
- Cards that care about the order of the library
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting topdeck effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different topdeck patterns
|
|
text_mask = create_topdeck_text_mask(df)
|
|
keyword_mask = create_topdeck_keyword_mask(df)
|
|
specific_mask = create_topdeck_specific_mask(df)
|
|
exclusion_mask = create_topdeck_exclusion_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = (text_mask | keyword_mask | specific_mask) & ~exclusion_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Topdeck'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with topdeck effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_topdeck: {str(e)}')
|
|
raise
|
|
|
|
## X Spells
|
|
def create_x_spells_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with X spell-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have X spell text patterns
|
|
"""
|
|
text_patterns = [
|
|
'cost {x} less',
|
|
'don\'t lose this',
|
|
'don\'t lose unspent',
|
|
'lose unused mana',
|
|
'unused mana would empty',
|
|
'with {x} in its',
|
|
'you cast cost {1} less',
|
|
'you cast cost {2} less',
|
|
'you cast cost {3} less',
|
|
'you cast cost {4} less',
|
|
'you cast cost {5} less'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_x_spells_mana_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with X in their mana cost.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have X in mana cost
|
|
"""
|
|
return df['manaCost'].fillna('').str.contains('{X}', case=True, regex=False)
|
|
|
|
def tag_for_x_spells(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that care about X spells using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Have X in their mana cost
|
|
- Care about X spells or mana values
|
|
- Have cost reduction effects for X spells
|
|
- Preserve unspent mana
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting X spells tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'manaCost'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different X spell patterns
|
|
text_mask = create_x_spells_text_mask(df)
|
|
mana_mask = create_x_spells_mana_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask | mana_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['X Spells'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with X spell effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_x_spells: {str(e)}')
|
|
raise
|
|
|
|
### Interaction
|
|
## Overall tag for interaction group
|
|
def tag_for_interaction(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that interact with the board state or stack.
|
|
|
|
This function coordinates tagging of different interaction types including:
|
|
- Counterspells
|
|
- Board wipes
|
|
- Combat tricks
|
|
- Protection effects
|
|
- Spot removal
|
|
|
|
The function maintains proper tag hierarchy and ensures consistent application
|
|
of interaction-related tags.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting interaction effect tagging for {color}_cards.csv')
|
|
print('\n==========\n')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'name', 'type', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Process each type of interaction
|
|
sub_start = pd.Timestamp.now()
|
|
tag_for_counterspells(df, color)
|
|
logger.info(f'Completed counterspell tagging in {(pd.Timestamp.now() - sub_start).total_seconds():.2f}s')
|
|
print('\n==========\n')
|
|
|
|
sub_start = pd.Timestamp.now()
|
|
tag_for_board_wipes(df, color)
|
|
logger.info(f'Completed board wipe tagging in {(pd.Timestamp.now() - sub_start).total_seconds():.2f}s')
|
|
print('\n==========\n')
|
|
|
|
sub_start = pd.Timestamp.now()
|
|
tag_for_combat_tricks(df, color)
|
|
logger.info(f'Completed combat trick tagging in {(pd.Timestamp.now() - sub_start).total_seconds():.2f}s')
|
|
print('\n==========\n')
|
|
|
|
sub_start = pd.Timestamp.now()
|
|
tag_for_protection(df, color)
|
|
logger.info(f'Completed protection tagging in {(pd.Timestamp.now() - sub_start).total_seconds():.2f}s')
|
|
print('\n==========\n')
|
|
|
|
sub_start = pd.Timestamp.now()
|
|
tag_for_removal(df, color)
|
|
logger.info(f'Completed removal tagging in {(pd.Timestamp.now() - sub_start).total_seconds():.2f}s')
|
|
print('\n==========\n')
|
|
|
|
# Log completion and performance metrics
|
|
duration = pd.Timestamp.now() - start_time
|
|
logger.info(f'Completed all interaction tagging in {duration.total_seconds():.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_interaction: {str(e)}')
|
|
raise
|
|
|
|
## Counterspells
|
|
def create_counterspell_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with counterspell text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have counterspell text patterns
|
|
"""
|
|
return tag_utils.create_text_mask(df, settings.COUNTERSPELL_TEXT_PATTERNS)
|
|
|
|
def create_counterspell_specific_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for specific counterspell cards.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are specific counterspell cards
|
|
"""
|
|
return tag_utils.create_name_mask(df, settings.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.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards should be excluded
|
|
"""
|
|
return tag_utils.create_text_mask(df, settings.COUNTERSPELL_EXCLUSION_PATTERNS)
|
|
|
|
def tag_for_counterspells(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that counter spells using vectorized operations.
|
|
|
|
This function identifies and tags cards that:
|
|
- Counter spells directly
|
|
- Return spells to hand/library
|
|
- Exile spells from the stack
|
|
- Care about countering spells
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting counterspell effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'name'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different counterspell patterns
|
|
text_mask = create_counterspell_text_mask(df)
|
|
specific_mask = create_counterspell_specific_mask(df)
|
|
exclusion_mask = create_counterspell_exclusion_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = (text_mask | specific_mask) & ~exclusion_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Counterspells', 'Interaction', 'Spellslinger', 'Spells Matter'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with counterspell effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_counterspells: {str(e)}')
|
|
raise
|
|
|
|
## Board Wipes
|
|
def tag_for_board_wipes(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that have board wipe effects using vectorized operations.
|
|
|
|
This function identifies and tags cards with board wipe effects including:
|
|
- Mass destruction effects (destroy all/each)
|
|
- Mass exile effects (exile all/each)
|
|
- Mass bounce effects (return all/each)
|
|
- Mass sacrifice effects (sacrifice all/each)
|
|
- Mass damage effects (damage to all/each)
|
|
|
|
The function uses helper functions to identify different types of board wipes
|
|
and applies tags consistently using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting board wipe effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'name'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different board wipe types
|
|
destroy_mask = tag_utils.create_mass_effect_mask(df, 'mass_destruction')
|
|
exile_mask = tag_utils.create_mass_effect_mask(df, 'mass_exile')
|
|
bounce_mask = tag_utils.create_mass_effect_mask(df, 'mass_bounce')
|
|
sacrifice_mask = tag_utils.create_mass_effect_mask(df, 'mass_sacrifice')
|
|
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)
|
|
|
|
# Create specific cards mask
|
|
specific_mask = tag_utils.create_name_mask(df, settings.BOARD_WIPE_SPECIFIC_CARDS)
|
|
|
|
# Combine all masks
|
|
final_mask = (
|
|
destroy_mask | exile_mask | bounce_mask |
|
|
sacrifice_mask | damage_mask | specific_mask
|
|
) & ~exclusion_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Board Wipes', 'Interaction'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with board wipe effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_board_wipes: {str(e)}')
|
|
raise
|
|
|
|
logger.info(f'Completed board wipe tagging for {color}_cards.csv')
|
|
|
|
## Combat Tricks
|
|
def create_combat_tricks_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with combat trick text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have combat trick text patterns
|
|
"""
|
|
# Create patterns for power/toughness modifiers
|
|
number_patterns = [str(x) for x in range(11)] + ['X']
|
|
buff_patterns = []
|
|
|
|
for num in number_patterns:
|
|
# Positive buffs
|
|
buff_patterns.extend([
|
|
fr'gets \+{num}/\+{num}',
|
|
fr'get \+{num}/\+{num}',
|
|
fr'gets \+{num}/\+0',
|
|
fr'get \+{num}/\+0',
|
|
fr'gets \+0/\+{num}',
|
|
fr'get \+0/\+{num}'
|
|
])
|
|
|
|
# Negative buffs
|
|
buff_patterns.extend([
|
|
fr'gets -{num}/-{num}',
|
|
fr'get -{num}/-{num}',
|
|
fr'gets -{num}/\+0',
|
|
fr'get -{num}/\+0',
|
|
fr'gets \+0/-{num}',
|
|
fr'get \+0/-{num}'
|
|
])
|
|
|
|
# Other combat trick patterns
|
|
other_patterns = [
|
|
'bolster',
|
|
'double strike',
|
|
'first strike',
|
|
'has base power and toughness',
|
|
'untap all creatures',
|
|
'untap target creature',
|
|
'with base power and toughness'
|
|
]
|
|
|
|
# Combine all patterns
|
|
all_patterns = buff_patterns + other_patterns
|
|
return tag_utils.create_text_mask(df, all_patterns)
|
|
|
|
def create_combat_tricks_type_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for instant-speed combat tricks.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards are instant-speed combat tricks
|
|
"""
|
|
return tag_utils.create_type_mask(df, 'Instant')
|
|
|
|
def create_combat_tricks_flash_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for flash-based combat tricks.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have flash-based combat tricks
|
|
"""
|
|
return tag_utils.create_keyword_mask(df, 'Flash')
|
|
|
|
def create_combat_tricks_exclusion_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that should be excluded from combat tricks.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards should be excluded
|
|
"""
|
|
# Specific cards to exclude
|
|
excluded_cards = [
|
|
'Assimilate Essence',
|
|
'Mantle of Leadership',
|
|
'Michiko\'s Reign of Truth // Portrait of Michiko'
|
|
]
|
|
name_mask = tag_utils.create_name_mask(df, excluded_cards)
|
|
|
|
# Text patterns to exclude
|
|
text_patterns = [
|
|
'remains tapped',
|
|
'only as a sorcery'
|
|
]
|
|
text_mask = tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
return name_mask | text_mask
|
|
|
|
def tag_for_combat_tricks(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that function as combat tricks using vectorized operations.
|
|
|
|
This function identifies and tags cards that modify combat through:
|
|
- Power/toughness buffs at instant speed
|
|
- Flash creatures and enchantments with combat effects
|
|
- Tap abilities that modify power/toughness
|
|
- Combat-relevant keywords and abilities
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting combat trick tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'type', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different combat trick patterns
|
|
text_mask = create_combat_tricks_text_mask(df)
|
|
type_mask = create_combat_tricks_type_mask(df)
|
|
flash_mask = create_combat_tricks_flash_mask(df)
|
|
exclusion_mask = create_combat_tricks_exclusion_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = ((text_mask & (type_mask | flash_mask)) |
|
|
(flash_mask & tag_utils.create_type_mask(df, 'Enchantment'))) & ~exclusion_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Combat Tricks', 'Interaction'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with combat trick effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_combat_tricks: {str(e)}')
|
|
raise
|
|
|
|
## Protection/Safety spells
|
|
def create_protection_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with protection-related text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have protection text patterns
|
|
"""
|
|
text_patterns = [
|
|
'has indestructible',
|
|
'has protection',
|
|
'has shroud',
|
|
'has ward',
|
|
'have indestructible',
|
|
'have protection',
|
|
'have shroud',
|
|
'have ward',
|
|
'hexproof from',
|
|
'gain hexproof',
|
|
'gain indestructible',
|
|
'gain protection',
|
|
'gain shroud',
|
|
'gain ward',
|
|
'gains hexproof',
|
|
'gains indestructible',
|
|
'gains protection',
|
|
'gains shroud',
|
|
'gains ward',
|
|
'phases out',
|
|
'protection from'
|
|
]
|
|
return tag_utils.create_text_mask(df, text_patterns)
|
|
|
|
def create_protection_keyword_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with protection-related keywords.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have protection keywords
|
|
"""
|
|
keyword_patterns = [
|
|
'Hexproof',
|
|
'Indestructible',
|
|
'Protection',
|
|
'Shroud',
|
|
'Ward'
|
|
]
|
|
return tag_utils.create_keyword_mask(df, keyword_patterns)
|
|
|
|
def create_protection_exclusion_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards that should be excluded from protection effects.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards should be excluded
|
|
"""
|
|
excluded_cards = [
|
|
'Out of Time',
|
|
'The War Doctor'
|
|
]
|
|
return tag_utils.create_name_mask(df, excluded_cards)
|
|
|
|
def tag_for_protection(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that provide or have protection effects using vectorized operations.
|
|
|
|
This function identifies and tags cards with protection effects including:
|
|
- Indestructible
|
|
- Protection from [quality]
|
|
- Hexproof/Shroud
|
|
- Ward
|
|
- Phase out
|
|
|
|
The function uses helper functions to identify different types of protection
|
|
and applies tags consistently using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting protection effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different protection patterns
|
|
text_mask = create_protection_text_mask(df)
|
|
keyword_mask = create_protection_keyword_mask(df)
|
|
exclusion_mask = create_protection_exclusion_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = (text_mask | keyword_mask) & ~exclusion_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Protection', 'Interaction'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with protection effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_protection: {str(e)}')
|
|
raise
|
|
|
|
## Spot removal
|
|
def create_removal_text_mask(df: pd.DataFrame) -> pd.Series:
|
|
"""Create a boolean mask for cards with removal text patterns.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards have removal text patterns
|
|
"""
|
|
return tag_utils.create_text_mask(df, settings.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.
|
|
|
|
Args:
|
|
df: DataFrame to search
|
|
|
|
Returns:
|
|
Boolean Series indicating which cards should be excluded
|
|
"""
|
|
return tag_utils.create_text_mask(df, settings.REMOVAL_EXCLUSION_PATTERNS)
|
|
|
|
|
|
def tag_for_removal(df: pd.DataFrame, color: str) -> None:
|
|
"""Tag cards that provide spot removal using vectorized operations.
|
|
|
|
This function identifies and tags cards that remove permanents through:
|
|
- Destroy effects
|
|
- Exile effects
|
|
- Bounce effects
|
|
- Sacrifice effects
|
|
|
|
The function uses helper functions to identify different types of removal
|
|
and applies tags consistently using vectorized operations.
|
|
|
|
Args:
|
|
df: DataFrame containing card data
|
|
color: Color identifier for logging purposes
|
|
|
|
Raises:
|
|
ValueError: If required DataFrame columns are missing
|
|
TypeError: If inputs are not of correct type
|
|
"""
|
|
start_time = pd.Timestamp.now()
|
|
logger.info(f'Starting removal effect tagging for {color}_cards.csv')
|
|
|
|
try:
|
|
# Validate inputs
|
|
if not isinstance(df, pd.DataFrame):
|
|
raise TypeError("df must be a pandas DataFrame")
|
|
if not isinstance(color, str):
|
|
raise TypeError("color must be a string")
|
|
|
|
# Validate required columns
|
|
required_cols = {'text', 'themeTags', 'keywords'}
|
|
tag_utils.validate_dataframe_columns(df, required_cols)
|
|
|
|
# Create masks for different removal patterns
|
|
text_mask = create_removal_text_mask(df)
|
|
|
|
# Combine masks
|
|
final_mask = text_mask
|
|
|
|
# Apply tags
|
|
tag_utils.apply_tag_vectorized(df, final_mask, ['Removal', 'Interaction'])
|
|
|
|
# Log results
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged {final_mask.sum()} cards with removal effects in {duration:.2f}s')
|
|
|
|
except Exception as e:
|
|
logger.error(f'Error in tag_for_removal: {str(e)}')
|
|
raise
|
|
|
|
def run_tagging():
|
|
start_time = pd.Timestamp.now()
|
|
for color in settings.COLORS:
|
|
load_dataframe(color)
|
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
|
logger.info(f'Tagged cards in {duration:.2f}s')
|
|
|