mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-17 16:10:12 +01:00
Finished refactoring and adding docstrings functions.
Added module-level docstrings to modules and cleaned up imports
This commit is contained in:
parent
8936fa347f
commit
c628b054ea
8 changed files with 784 additions and 252 deletions
253
builder_utils.py
253
builder_utils.py
|
|
@ -1,49 +1,48 @@
|
||||||
from typing import Dict, List, Tuple, Optional, Any, Callable, TypeVar, Union, cast
|
"""Utility module for MTG deck building operations.
|
||||||
import pandas as pd
|
|
||||||
from price_check import PriceChecker
|
This module provides utility functions for various deck building operations including:
|
||||||
from input_handler import InputHandler
|
- DataFrame validation and processing
|
||||||
import logging
|
- Card type counting and validation
|
||||||
|
- Land selection and management
|
||||||
|
- Theme processing and weighting
|
||||||
|
- Price checking integration
|
||||||
|
- Mana pip analysis
|
||||||
|
|
||||||
|
The module serves as a central collection of helper functions used throughout the
|
||||||
|
deck building process, handling data validation, card selection, and various
|
||||||
|
deck composition calculations.
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- DataFrame validation with timeout handling
|
||||||
|
- Card type counting and categorization
|
||||||
|
- Land type validation and selection (basic, fetch, dual, etc.)
|
||||||
|
- Theme tag processing and weighting calculations
|
||||||
|
- Mana pip counting and color distribution analysis
|
||||||
|
|
||||||
|
Typical usage example:
|
||||||
|
>>> df = load_commander_data()
|
||||||
|
>>> validate_dataframe(df, DATAFRAME_VALIDATION_RULES)
|
||||||
|
>>> process_dataframe_batch(df)
|
||||||
|
>>> count_cards_by_type(df, ['Creature', 'Instant', 'Sorcery'])
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Standard library imports
|
||||||
import functools
|
import functools
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union, cast
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from fuzzywuzzy import process
|
from fuzzywuzzy import process
|
||||||
from settings import (
|
|
||||||
COMMANDER_CSV_PATH,
|
# Local application imports
|
||||||
FUZZY_MATCH_THRESHOLD,
|
|
||||||
MAX_FUZZY_CHOICES,
|
|
||||||
COMMANDER_CONVERTERS,
|
|
||||||
DATAFRAME_VALIDATION_RULES,
|
|
||||||
DATAFRAME_VALIDATION_TIMEOUT,
|
|
||||||
DATAFRAME_BATCH_SIZE,
|
|
||||||
DATAFRAME_TRANSFORM_TIMEOUT,
|
|
||||||
DATAFRAME_REQUIRED_COLUMNS,
|
|
||||||
WEIGHT_ADJUSTMENT_FACTORS,
|
|
||||||
DEFAULT_MAX_DECK_PRICE,
|
|
||||||
DEFAULT_MAX_CARD_PRICE,
|
|
||||||
DECK_COMPOSITION_PROMPTS,
|
|
||||||
DEFAULT_RAMP_COUNT,
|
|
||||||
DEFAULT_LAND_COUNT,
|
|
||||||
DEFAULT_BASIC_LAND_COUNT,
|
|
||||||
DEFAULT_CREATURE_COUNT,
|
|
||||||
DEFAULT_REMOVAL_COUNT,
|
|
||||||
DEFAULT_CARD_ADVANTAGE_COUNT,
|
|
||||||
DEFAULT_PROTECTION_COUNT,
|
|
||||||
DEFAULT_WIPES_COUNT,
|
|
||||||
CARD_TYPE_SORT_ORDER,
|
|
||||||
DUPLICATE_CARD_FORMAT,
|
|
||||||
COLOR_TO_BASIC_LAND,
|
|
||||||
SNOW_BASIC_LAND_MAPPING,
|
|
||||||
KINDRED_STAPLE_LANDS,
|
|
||||||
DUAL_LAND_TYPE_MAP,
|
|
||||||
MANA_COLORS,
|
|
||||||
MANA_PIP_PATTERNS
|
|
||||||
)
|
|
||||||
from exceptions import (
|
from exceptions import (
|
||||||
|
CSVValidationError,
|
||||||
|
DataFrameTimeoutError,
|
||||||
|
DataFrameValidationError,
|
||||||
DeckBuilderError,
|
DeckBuilderError,
|
||||||
DuplicateCardError,
|
DuplicateCardError,
|
||||||
CSVValidationError,
|
|
||||||
DataFrameValidationError,
|
|
||||||
DataFrameTimeoutError,
|
|
||||||
EmptyDataFrameError,
|
EmptyDataFrameError,
|
||||||
FetchLandSelectionError,
|
FetchLandSelectionError,
|
||||||
FetchLandValidationError,
|
FetchLandValidationError,
|
||||||
|
|
@ -54,6 +53,24 @@ from exceptions import (
|
||||||
ThemeWeightError,
|
ThemeWeightError,
|
||||||
CardTypeCountError
|
CardTypeCountError
|
||||||
)
|
)
|
||||||
|
from input_handler import InputHandler
|
||||||
|
from price_check import PriceChecker
|
||||||
|
from settings import (
|
||||||
|
CARD_TYPE_SORT_ORDER, COLOR_TO_BASIC_LAND, COMMANDER_CONVERTERS,
|
||||||
|
COMMANDER_CSV_PATH, DATAFRAME_BATCH_SIZE,
|
||||||
|
DATAFRAME_REQUIRED_COLUMNS, DATAFRAME_TRANSFORM_TIMEOUT,
|
||||||
|
DATAFRAME_VALIDATION_RULES, DATAFRAME_VALIDATION_TIMEOUT,
|
||||||
|
DECK_COMPOSITION_PROMPTS, DEFAULT_BASIC_LAND_COUNT,
|
||||||
|
DEFAULT_CARD_ADVANTAGE_COUNT, DEFAULT_CREATURE_COUNT,
|
||||||
|
DEFAULT_LAND_COUNT, DEFAULT_MAX_CARD_PRICE, DEFAULT_MAX_DECK_PRICE,
|
||||||
|
DEFAULT_PROTECTION_COUNT, DEFAULT_RAMP_COUNT,
|
||||||
|
DEFAULT_REMOVAL_COUNT, DEFAULT_WIPES_COUNT, DUAL_LAND_TYPE_MAP,
|
||||||
|
DUPLICATE_CARD_FORMAT, FUZZY_MATCH_THRESHOLD, KINDRED_STAPLE_LANDS,
|
||||||
|
MANA_COLORS, MANA_PIP_PATTERNS, MAX_FUZZY_CHOICES,
|
||||||
|
SNOW_BASIC_LAND_MAPPING, THEME_POOL_SIZE_MULTIPLIER,
|
||||||
|
WEIGHT_ADJUSTMENT_FACTORS
|
||||||
|
)
|
||||||
|
from type_definitions import CardLibraryDF, CommanderDF, LandDF
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
|
@ -61,7 +78,6 @@ logging.basicConfig(
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Type variables for generic functions
|
# Type variables for generic functions
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
DataFrame = TypeVar('DataFrame', bound=pd.DataFrame)
|
DataFrame = TypeVar('DataFrame', bound=pd.DataFrame)
|
||||||
|
|
@ -431,7 +447,6 @@ def adjust_theme_weights(primary_theme: str,
|
||||||
if total_weight > 0:
|
if total_weight > 0:
|
||||||
adjusted_weights = {k: round(v/total_weight, 2) for k, v in adjusted_weights.items()}
|
adjusted_weights = {k: round(v/total_weight, 2) for k, v in adjusted_weights.items()}
|
||||||
|
|
||||||
print(adjusted_weights)
|
|
||||||
return adjusted_weights
|
return adjusted_weights
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -1404,6 +1419,162 @@ def select_land_for_removal(filtered_lands: pd.DataFrame) -> Tuple[int, str]:
|
||||||
logger.error(f"Error selecting land for removal: {e}")
|
logger.error(f"Error selecting land for removal: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def get_card_theme_overlap(card_tags: List[str], deck_themes: List[str]) -> int:
|
||||||
|
"""Count how many deck themes a given card matches.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
card_tags: List of tags associated with the card
|
||||||
|
deck_themes: List of themes in the deck
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of deck themes that match the card's tags
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> card_tags = ['Artifacts Matter', 'Token Creation', 'Sacrifice']
|
||||||
|
>>> deck_themes = ['Artifacts Matter', 'Sacrifice Matters']
|
||||||
|
>>> get_card_theme_overlap(card_tags, deck_themes)
|
||||||
|
2
|
||||||
|
"""
|
||||||
|
if not card_tags or not deck_themes:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Convert to sets for efficient intersection
|
||||||
|
card_tag_set = set(card_tags)
|
||||||
|
deck_theme_set = set(deck_themes)
|
||||||
|
|
||||||
|
# Count overlapping themes
|
||||||
|
return len(card_tag_set.intersection(deck_theme_set))
|
||||||
|
|
||||||
|
def calculate_theme_priority(card_tags: List[str], deck_themes: List[str], THEME_PRIORITY_BONUS: float) -> float:
|
||||||
|
"""Calculate priority score for a card based on theme overlap.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
card_tags: List of tags associated with the card
|
||||||
|
deck_themes: List of themes in the deck
|
||||||
|
THEME_PRIORITY_BONUS: Bonus multiplier for each additional theme match
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Priority score for the card (higher means more theme overlap)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> card_tags = ['Artifacts Matter', 'Token Creation', 'Sacrifice']
|
||||||
|
>>> deck_themes = ['Artifacts Matter', 'Sacrifice Matters']
|
||||||
|
>>> calculate_theme_priority(card_tags, deck_themes, 1.2)
|
||||||
|
1.44 # Base score of 1.0 * (1.2 ^ 2) for two theme matches
|
||||||
|
"""
|
||||||
|
overlap_count = get_card_theme_overlap(card_tags, deck_themes)
|
||||||
|
if overlap_count == 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
# Calculate priority score with exponential bonus for multiple matches
|
||||||
|
return pow(THEME_PRIORITY_BONUS, overlap_count)
|
||||||
|
|
||||||
|
def calculate_weighted_pool_size(ideal_count: int, weight: float, multiplier: float = THEME_POOL_SIZE_MULTIPLIER) -> int:
|
||||||
|
"""Calculate the size of the initial card pool based on ideal count and weight.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ideal_count: Target number of cards to select
|
||||||
|
weight: Theme weight factor (0.0-1.0)
|
||||||
|
multiplier: Pool size multiplier (default from settings)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Calculated pool size
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> calculate_weighted_pool_size(10, 0.8, 2.0)
|
||||||
|
16
|
||||||
|
"""
|
||||||
|
return int(ideal_count * weight * multiplier)
|
||||||
|
|
||||||
|
def filter_theme_cards(df: pd.DataFrame, themes: List[str], pool_size: int) -> pd.DataFrame:
|
||||||
|
"""Filter cards by theme and return top cards by EDHREC rank.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: Source DataFrame to filter
|
||||||
|
themes: List of theme tags to filter by
|
||||||
|
pool_size: Number of cards to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered DataFrame with top cards
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If themes is None or contains invalid values
|
||||||
|
TypeError: If themes is not a list
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> filtered_df = filter_theme_cards(cards_df, ['Artifacts Matter', 'Token Creation'], 20)
|
||||||
|
"""
|
||||||
|
# Input validation
|
||||||
|
if themes is None:
|
||||||
|
raise ValueError("themes parameter cannot be None")
|
||||||
|
|
||||||
|
if not isinstance(themes, list):
|
||||||
|
raise TypeError("themes must be a list of strings")
|
||||||
|
|
||||||
|
if not all(isinstance(theme, str) for theme in themes):
|
||||||
|
raise ValueError("all themes must be strings")
|
||||||
|
|
||||||
|
if not themes:
|
||||||
|
return pd.DataFrame() # Return empty DataFrame for empty themes list
|
||||||
|
|
||||||
|
# Create copy to avoid modifying original
|
||||||
|
filtered_df = df.copy()
|
||||||
|
|
||||||
|
# Filter by theme
|
||||||
|
filtered_df = filtered_df[filtered_df['themeTags'].apply(
|
||||||
|
lambda x: any(theme in x for theme in themes) if isinstance(x, list) else False
|
||||||
|
)]
|
||||||
|
|
||||||
|
# Sort by EDHREC rank and take top cards
|
||||||
|
filtered_df.sort_values('edhrecRank', inplace=True)
|
||||||
|
return filtered_df.head(pool_size)
|
||||||
|
|
||||||
|
def select_weighted_cards(
|
||||||
|
card_pool: pd.DataFrame,
|
||||||
|
target_count: int,
|
||||||
|
price_checker: Optional[Any] = None,
|
||||||
|
max_price: Optional[float] = None
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Select cards from pool considering price constraints.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
card_pool: DataFrame of candidate cards
|
||||||
|
target_count: Number of cards to select
|
||||||
|
price_checker: Optional price checker instance
|
||||||
|
max_price: Maximum allowed price per card
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of selected card dictionaries
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> selected = select_weighted_cards(pool_df, 5, price_checker, 10.0)
|
||||||
|
"""
|
||||||
|
selected_cards = []
|
||||||
|
|
||||||
|
for _, card in card_pool.iterrows():
|
||||||
|
if len(selected_cards) >= target_count:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check price if enabled
|
||||||
|
if price_checker and max_price:
|
||||||
|
try:
|
||||||
|
price = price_checker.get_card_price(card['name'])
|
||||||
|
if price > max_price * 1.1:
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Price check failed for {card['name']}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
selected_cards.append({
|
||||||
|
'name': card['name'],
|
||||||
|
'type': card['type'],
|
||||||
|
'manaCost': card['manaCost'],
|
||||||
|
'manaValue': card['manaValue'],
|
||||||
|
'themeTags': card['themeTags']
|
||||||
|
})
|
||||||
|
|
||||||
|
return selected_cards
|
||||||
|
|
||||||
def count_color_pips(mana_costs: pd.Series, color: str) -> int:
|
def count_color_pips(mana_costs: pd.Series, color: str) -> int:
|
||||||
"""Count the number of colored mana pips of a specific color in mana costs.
|
"""Count the number of colored mana pips of a specific color in mana costs.
|
||||||
|
|
||||||
|
|
|
||||||
560
deck_builder.py
560
deck_builder.py
|
|
@ -20,16 +20,16 @@ from settings import (
|
||||||
COMMANDER_CSV_PATH, FUZZY_MATCH_THRESHOLD, MAX_FUZZY_CHOICES, FETCH_LAND_DEFAULT_COUNT,
|
COMMANDER_CSV_PATH, FUZZY_MATCH_THRESHOLD, MAX_FUZZY_CHOICES, FETCH_LAND_DEFAULT_COUNT,
|
||||||
COMMANDER_POWER_DEFAULT, COMMANDER_TOUGHNESS_DEFAULT, COMMANDER_MANA_COST_DEFAULT,
|
COMMANDER_POWER_DEFAULT, COMMANDER_TOUGHNESS_DEFAULT, COMMANDER_MANA_COST_DEFAULT,
|
||||||
COMMANDER_MANA_VALUE_DEFAULT, COMMANDER_TYPE_DEFAULT, COMMANDER_TEXT_DEFAULT,
|
COMMANDER_MANA_VALUE_DEFAULT, COMMANDER_TYPE_DEFAULT, COMMANDER_TEXT_DEFAULT,
|
||||||
|
THEME_PRIORITY_BONUS, THEME_POOL_SIZE_MULTIPLIER,
|
||||||
COMMANDER_COLOR_IDENTITY_DEFAULT, COMMANDER_COLORS_DEFAULT, COMMANDER_TAGS_DEFAULT,
|
COMMANDER_COLOR_IDENTITY_DEFAULT, COMMANDER_COLORS_DEFAULT, COMMANDER_TAGS_DEFAULT,
|
||||||
COMMANDER_THEMES_DEFAULT, COMMANDER_CREATURE_TYPES_DEFAULT, DUAL_LAND_TYPE_MAP,
|
COMMANDER_THEMES_DEFAULT, COMMANDER_CREATURE_TYPES_DEFAULT, DUAL_LAND_TYPE_MAP,
|
||||||
CSV_READ_TIMEOUT, CSV_PROCESSING_BATCH_SIZE, CSV_VALIDATION_RULES, CSV_REQUIRED_COLUMNS,
|
CSV_READ_TIMEOUT, CSV_PROCESSING_BATCH_SIZE, CSV_VALIDATION_RULES, CSV_REQUIRED_COLUMNS,
|
||||||
STAPLE_LAND_CONDITIONS, TRIPLE_LAND_TYPE_MAP, MISC_LAND_MAX_COUNT, MISC_LAND_MIN_COUNT,
|
STAPLE_LAND_CONDITIONS, TRIPLE_LAND_TYPE_MAP, MISC_LAND_MAX_COUNT, MISC_LAND_MIN_COUNT,
|
||||||
MISC_LAND_POOL_SIZE, LAND_REMOVAL_MAX_ATTEMPTS, PROTECTED_LANDS,
|
MISC_LAND_POOL_SIZE, LAND_REMOVAL_MAX_ATTEMPTS, PROTECTED_LANDS,
|
||||||
MANA_COLORS, MANA_PIP_PATTERNS
|
MANA_COLORS, MANA_PIP_PATTERNS, THEME_WEIGHT_MULTIPLIER
|
||||||
)
|
)
|
||||||
import builder_utils
|
import builder_utils
|
||||||
import setup_utils
|
import setup_utils
|
||||||
from setup import determine_commanders
|
|
||||||
from input_handler import InputHandler
|
from input_handler import InputHandler
|
||||||
from exceptions import (
|
from exceptions import (
|
||||||
BasicLandCountError,
|
BasicLandCountError,
|
||||||
|
|
@ -37,7 +37,6 @@ from exceptions import (
|
||||||
CommanderMoveError,
|
CommanderMoveError,
|
||||||
CardTypeCountError,
|
CardTypeCountError,
|
||||||
CommanderColorError,
|
CommanderColorError,
|
||||||
CommanderLoadError,
|
|
||||||
CommanderSelectionError,
|
CommanderSelectionError,
|
||||||
CommanderValidationError,
|
CommanderValidationError,
|
||||||
CSVError,
|
CSVError,
|
||||||
|
|
@ -48,16 +47,12 @@ from exceptions import (
|
||||||
DuplicateCardError,
|
DuplicateCardError,
|
||||||
DeckBuilderError,
|
DeckBuilderError,
|
||||||
EmptyDataFrameError,
|
EmptyDataFrameError,
|
||||||
EmptyInputError,
|
|
||||||
FetchLandSelectionError,
|
FetchLandSelectionError,
|
||||||
FetchLandValidationError,
|
FetchLandValidationError,
|
||||||
IdealDeterminationError,
|
IdealDeterminationError,
|
||||||
InvalidNumberError,
|
|
||||||
InvalidQuestionTypeError,
|
|
||||||
LandRemovalError,
|
LandRemovalError,
|
||||||
LibraryOrganizationError,
|
LibraryOrganizationError,
|
||||||
LibrarySortError,
|
LibrarySortError,
|
||||||
MaxAttemptsError,
|
|
||||||
PriceAPIError,
|
PriceAPIError,
|
||||||
PriceConfigurationError,
|
PriceConfigurationError,
|
||||||
PriceLimitError,
|
PriceLimitError,
|
||||||
|
|
@ -66,18 +61,21 @@ from exceptions import (
|
||||||
ThemeSelectionError,
|
ThemeSelectionError,
|
||||||
ThemeWeightError,
|
ThemeWeightError,
|
||||||
StapleLandError,
|
StapleLandError,
|
||||||
StapleLandError,
|
ManaPipError,
|
||||||
ManaPipError
|
ThemeTagError,
|
||||||
|
ThemeWeightingError,
|
||||||
|
ThemePoolError
|
||||||
)
|
)
|
||||||
from type_definitions import (
|
from type_definitions import (
|
||||||
CardDict,
|
|
||||||
CommanderDict,
|
CommanderDict,
|
||||||
CardLibraryDF,
|
CardLibraryDF,
|
||||||
CommanderDF,
|
CommanderDF,
|
||||||
LandDF,
|
LandDF,
|
||||||
ArtifactDF,
|
ArtifactDF,
|
||||||
CreatureDF,
|
CreatureDF,
|
||||||
NonCreatureDF)
|
NonCreatureDF,
|
||||||
|
PlaneswalkerDF,
|
||||||
|
NonPlaneswalkerDF)
|
||||||
|
|
||||||
# Try to import scrython and price_checker
|
# Try to import scrython and price_checker
|
||||||
try:
|
try:
|
||||||
|
|
@ -102,19 +100,6 @@ pd.set_option('display.max_columns', None)
|
||||||
pd.set_option('display.max_rows', None)
|
pd.set_option('display.max_rows', None)
|
||||||
pd.set_option('display.max_colwidth', 50)
|
pd.set_option('display.max_colwidth', 50)
|
||||||
|
|
||||||
"""
|
|
||||||
Basic deck builder, primarily intended for building Kindred decks.
|
|
||||||
Logic for other themes (such as Spellslinger or Wheels), is added.
|
|
||||||
I plan to also implement having it recommend a commander or themes.
|
|
||||||
|
|
||||||
Currently, the script will ask questions to determine number of
|
|
||||||
creatures, lands, interaction, ramp, etc... then add cards and
|
|
||||||
adjust from there.
|
|
||||||
|
|
||||||
Land spread will ideally be handled based on pips and some adjustment
|
|
||||||
is planned based on mana curve and ramp added.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def new_line(num_lines: int = 1) -> None:
|
def new_line(num_lines: int = 1) -> None:
|
||||||
"""Print specified number of newlines for formatting output.
|
"""Print specified number of newlines for formatting output.
|
||||||
|
|
||||||
|
|
@ -149,9 +134,10 @@ class DeckBuilder:
|
||||||
self.artifact_df: ArtifactDF = pd.DataFrame()
|
self.artifact_df: ArtifactDF = pd.DataFrame()
|
||||||
self.creature_df: CreatureDF = pd.DataFrame()
|
self.creature_df: CreatureDF = pd.DataFrame()
|
||||||
self.noncreature_df: NonCreatureDF = pd.DataFrame()
|
self.noncreature_df: NonCreatureDF = pd.DataFrame()
|
||||||
|
self.nonplaneswalker_df: NonPlaneswalkerDF = pd.DataFrame()
|
||||||
# Initialize other attributes with type hints
|
# Initialize other attributes with type hints
|
||||||
self.commander_info: Dict = {}
|
self.commander_info: Dict = {}
|
||||||
|
self.max_card_price: Optional[float] = None
|
||||||
self.commander_dict: CommanderDict = {}
|
self.commander_dict: CommanderDict = {}
|
||||||
self.commander: str = ''
|
self.commander: str = ''
|
||||||
self.commander_type: str = ''
|
self.commander_type: str = ''
|
||||||
|
|
@ -764,7 +750,7 @@ class DeckBuilder:
|
||||||
|
|
||||||
# Remove lands from main DataFrame
|
# Remove lands from main DataFrame
|
||||||
df = df[~df['type'].str.contains('Land')]
|
df = df[~df['type'].str.contains('Land')]
|
||||||
df.to_csv(f'{CSV_DIRECTORY}/test_cards.csv')
|
df.to_csv(f'{CSV_DIRECTORY}/test_cards.csv', index=False)
|
||||||
|
|
||||||
# Create specialized frames
|
# Create specialized frames
|
||||||
self.artifact_df = df[df['type'].str.contains('Artifact')].copy()
|
self.artifact_df = df[df['type'].str.contains('Artifact')].copy()
|
||||||
|
|
@ -774,9 +760,10 @@ class DeckBuilder:
|
||||||
self.enchantment_df = df[df['type'].str.contains('Enchantment')].copy()
|
self.enchantment_df = df[df['type'].str.contains('Enchantment')].copy()
|
||||||
self.instant_df = df[df['type'].str.contains('Instant')].copy()
|
self.instant_df = df[df['type'].str.contains('Instant')].copy()
|
||||||
self.planeswalker_df = df[df['type'].str.contains('Planeswalker')].copy()
|
self.planeswalker_df = df[df['type'].str.contains('Planeswalker')].copy()
|
||||||
|
self.nonplaneswalker_df = df[~df['type'].str.contains('Planeswalker')].copy()
|
||||||
self.sorcery_df = df[df['type'].str.contains('Sorcery')].copy()
|
self.sorcery_df = df[df['type'].str.contains('Sorcery')].copy()
|
||||||
|
|
||||||
self.battle_df.to_csv(f'{CSV_DIRECTORY}/test_battle_cards.csv')
|
self.battle_df.to_csv(f'{CSV_DIRECTORY}/test_battle_cards.csv', index=False)
|
||||||
|
|
||||||
# Sort all frames
|
# Sort all frames
|
||||||
for frame in [self.artifact_df, self.battle_df, self.creature_df,
|
for frame in [self.artifact_df, self.battle_df, self.creature_df,
|
||||||
|
|
@ -859,8 +846,9 @@ class DeckBuilder:
|
||||||
try:
|
try:
|
||||||
# Load and combine data
|
# Load and combine data
|
||||||
self.full_df = self._load_and_combine_data()
|
self.full_df = self._load_and_combine_data()
|
||||||
|
self.full_df = self.full_df[~self.full_df['name'].str.contains(self.commander)]
|
||||||
self.full_df.sort_values(by='edhrecRank', inplace=True)
|
self.full_df.sort_values(by='edhrecRank', inplace=True)
|
||||||
self.full_df.to_csv(f'{CSV_DIRECTORY}/test_full_cards.csv')
|
self.full_df.to_csv(f'{CSV_DIRECTORY}/test_full_cards.csv', index=False)
|
||||||
|
|
||||||
# Split into specialized frames
|
# Split into specialized frames
|
||||||
self._split_into_specialized_frames(self.full_df)
|
self._split_into_specialized_frames(self.full_df)
|
||||||
|
|
@ -962,7 +950,6 @@ class DeckBuilder:
|
||||||
self.tertiary_theme,
|
self.tertiary_theme,
|
||||||
self.weights
|
self.weights
|
||||||
)
|
)
|
||||||
print(self.weights)
|
|
||||||
self.primary_weight = self.weights['primary']
|
self.primary_weight = self.weights['primary']
|
||||||
self.secondary_weight = self.weights['secondary']
|
self.secondary_weight = self.weights['secondary']
|
||||||
self.tertiary_weight = self.weights['tertiary']
|
self.tertiary_weight = self.weights['tertiary']
|
||||||
|
|
@ -972,8 +959,7 @@ class DeckBuilder:
|
||||||
if self.secondary_theme:
|
if self.secondary_theme:
|
||||||
self.themes.append(self.secondary_theme)
|
self.themes.append(self.secondary_theme)
|
||||||
if self.tertiary_theme:
|
if self.tertiary_theme:
|
||||||
self.themes.append(self.tertiary_theme)
|
self.themes.append
|
||||||
print(self.weights)
|
|
||||||
self.determine_hidden_themes()
|
self.determine_hidden_themes()
|
||||||
|
|
||||||
except (ThemeSelectionError, ThemeWeightError) as e:
|
except (ThemeSelectionError, ThemeWeightError) as e:
|
||||||
|
|
@ -1323,7 +1309,7 @@ class DeckBuilder:
|
||||||
6. Add miscellaneous utility lands
|
6. Add miscellaneous utility lands
|
||||||
7. Adjust total land count to match ideal count
|
7. Adjust total land count to match ideal count
|
||||||
"""
|
"""
|
||||||
MAX_ADJUSTMENT_ATTEMPTS = 10
|
MAX_ADJUSTMENT_ATTEMPTS = (self.ideal_land_count - self.min_basics) * 1.5
|
||||||
self.total_basics = 0
|
self.total_basics = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -2032,222 +2018,395 @@ class DeckBuilder:
|
||||||
logger.error(f"Error calculating CMC: {e}")
|
logger.error(f"Error calculating CMC: {e}")
|
||||||
self.cmc = 0.0
|
self.cmc = 0.0
|
||||||
|
|
||||||
def weight_by_theme(self, tag, ideal=1, weight=1, df=None):
|
def weight_by_theme(self, tag: str, ideal: int = 1, weight: float = 1.0, df: Optional[pd.DataFrame] = None) -> None:
|
||||||
# First grab the first 50/30/20 cards that match each theme
|
"""Add cards with specific tag up to weighted ideal count.
|
||||||
"""Add cards with specific tag up to ideal_value count"""
|
|
||||||
ideal_value = math.ceil(ideal * weight * 0.9)
|
|
||||||
print(f'Finding {ideal_value} cards with the "{tag}" tag...')
|
|
||||||
if 'Kindred' in tag:
|
|
||||||
tags = [tag, 'Kindred Support']
|
|
||||||
else:
|
|
||||||
tags = [tag]
|
|
||||||
# Filter cards with the given tag
|
|
||||||
tag_df = df.copy()
|
|
||||||
tag_df.sort_values(by='edhrecRank', inplace=True)
|
|
||||||
tag_df = tag_df[tag_df['themeTags'].apply(lambda x: any(tag in x for tag in tags))]
|
|
||||||
# Take top cards based on ideal value
|
|
||||||
pool_size = int(ideal_value * random.randint(15, 20) /10)
|
|
||||||
tag_df = tag_df.head(pool_size)
|
|
||||||
|
|
||||||
# Convert to list of card dictionaries
|
Args:
|
||||||
card_pool = [
|
tag: Theme tag to filter cards by
|
||||||
{
|
ideal: Target number of cards to add
|
||||||
'name': row['name'],
|
weight: Theme weight factor (0.0-1.0)
|
||||||
'type': row['type'],
|
df: Source DataFrame to filter cards from
|
||||||
'manaCost': row['manaCost'],
|
|
||||||
'manaValue': row['manaValue'],
|
|
||||||
'creatureTypes': row['creatureTypes'],
|
|
||||||
'themeTags': row['themeTags']
|
|
||||||
}
|
|
||||||
for _, row in tag_df.iterrows()
|
|
||||||
]
|
|
||||||
|
|
||||||
# Randomly select cards up to ideal value
|
Raises:
|
||||||
cards_to_add = []
|
ThemeWeightingError: If weight calculation fails
|
||||||
while len(cards_to_add) < ideal_value and card_pool:
|
ThemePoolError: If card pool is empty or insufficient
|
||||||
card = random.choice(card_pool)
|
"""
|
||||||
card_pool.remove(card)
|
try:
|
||||||
|
# Calculate target card count using weight and safety multiplier
|
||||||
|
target_count = math.ceil(ideal * weight * THEME_WEIGHT_MULTIPLIER)
|
||||||
|
logger.info(f'Finding {target_count} cards with the "{tag}" tag...')
|
||||||
|
|
||||||
# Check price constraints if enabled
|
# Handle Kindred theme special case
|
||||||
if use_scrython and self.set_max_card_price:
|
tags = [tag, 'Kindred Support'] if 'Kindred' in tag else [tag]
|
||||||
price = self.price_checker.get_card_price(card['name'])
|
|
||||||
if price > self.max_card_price * 1.1:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Add card if not already in library
|
# Calculate initial pool size
|
||||||
|
pool_size = builder_utils.calculate_weighted_pool_size(target_count, weight)
|
||||||
|
|
||||||
if card['name'] in multiple_copy_cards:
|
# Filter cards by theme
|
||||||
if card['name'] == 'Nazgûl':
|
if df is None:
|
||||||
for _ in range(9):
|
raise ThemePoolError(f"No source DataFrame provided for theme {tag}")
|
||||||
cards_to_add.append(card)
|
|
||||||
elif card['name'] == 'Seven Dwarves':
|
tag_df = builder_utils.filter_theme_cards(df, tags, pool_size)
|
||||||
for _ in range(7):
|
if tag_df.empty:
|
||||||
cards_to_add.append(card)
|
raise ThemePoolError(f"No cards found for theme {tag}")
|
||||||
|
|
||||||
|
# Select cards considering price and duplicates
|
||||||
|
selected_cards = builder_utils.select_weighted_cards(
|
||||||
|
tag_df,
|
||||||
|
target_count,
|
||||||
|
self.price_checker if use_scrython else None,
|
||||||
|
self.max_card_price if hasattr(self, 'max_card_price') else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process selected cards
|
||||||
|
cards_added = []
|
||||||
|
for card in selected_cards:
|
||||||
|
# Handle multiple copy cards
|
||||||
|
if card['name'] in multiple_copy_cards:
|
||||||
|
copies = {
|
||||||
|
'Nazgûl': 9,
|
||||||
|
'Seven Dwarves': 7
|
||||||
|
}.get(card['name'], target_count - len(cards_added))
|
||||||
|
|
||||||
|
for _ in range(copies):
|
||||||
|
cards_added.append(card)
|
||||||
|
|
||||||
|
# Handle regular cards
|
||||||
|
elif card['name'] not in self.card_library['Card Name'].values:
|
||||||
|
cards_added.append(card)
|
||||||
else:
|
else:
|
||||||
num_to_add = ideal_value - len(cards_to_add)
|
logger.warning(f"{card['name']} already in Library, skipping it.")
|
||||||
for _ in range(num_to_add):
|
|
||||||
cards_to_add.append(card)
|
|
||||||
|
|
||||||
elif (card['name'] not in multiple_copy_cards
|
# Add selected cards to library
|
||||||
and card['name'] not in self.card_library['Card Name'].values):
|
for card in cards_added:
|
||||||
cards_to_add.append(card)
|
self.add_card(
|
||||||
|
card['name'],
|
||||||
|
card['type'],
|
||||||
|
card['manaCost'],
|
||||||
|
card['manaValue'],
|
||||||
|
card.get('creatureTypes'),
|
||||||
|
card['themeTags']
|
||||||
|
)
|
||||||
|
|
||||||
elif (card['name'] not in multiple_copy_cards
|
# Update DataFrames
|
||||||
and card['name'] in self.card_library['Card Name'].values):
|
used_cards = {card['name'] for card in selected_cards}
|
||||||
logger.warning(f"{card['name']} already in Library, skipping it.")
|
self.noncreature_df = self.noncreature_df[~self.noncreature_df['name'].isin(used_cards)]
|
||||||
continue
|
|
||||||
|
|
||||||
# Add selected cards to library
|
logger.info(f'Added {len(cards_added)} {tag} cards')
|
||||||
for card in cards_to_add:
|
for card in cards_added:
|
||||||
self.add_card(card['name'], card['type'],
|
print(card['name'])
|
||||||
card['manaCost'], card['manaValue'],
|
|
||||||
card['creatureTypes'], card['themeTags'])
|
|
||||||
|
|
||||||
card_pool_names = [item['name'] for item in card_pool]
|
except (ThemeWeightingError, ThemePoolError) as e:
|
||||||
self.full_df = self.full_df[~self.full_df['name'].isin(card_pool_names)]
|
logger.error(f"Error in weight_by_theme: {e}")
|
||||||
self.noncreature_df = self.noncreature_df[~self.noncreature_df['name'].isin(card_pool_names)]
|
raise
|
||||||
logger.info(f'Added {len(cards_to_add)} {tag} cards')
|
except Exception as e:
|
||||||
#tag_df.to_csv(f'{CSV_DIRECTORY}/test_{tag}.csv', index=False)
|
logger.error(f"Unexpected error in weight_by_theme: {e}")
|
||||||
|
raise ThemeWeightingError(f"Failed to process theme {tag}: {str(e)}")
|
||||||
|
|
||||||
def add_by_tags(self, tag, ideal_value=1, df=None):
|
def add_by_tags(self, tag, ideal_value=1, df=None, ignore_existing=False):
|
||||||
"""Add cards with specific tag up to ideal_value count"""
|
"""Add cards with specific tag up to ideal_value count.
|
||||||
print(f'Finding {ideal_value} cards with the "{tag}" tag...')
|
Args:
|
||||||
|
tag: The theme tag to filter cards by
|
||||||
|
ideal_value: Target number of cards to add
|
||||||
|
df: DataFrame containing candidate cards
|
||||||
|
|
||||||
# Filter cards with the given tag
|
Raises:
|
||||||
skip_creatures = self.creature_cards > self.ideal_creature_count * 1.1
|
ThemeTagError: If there are issues with tag processing or card selection
|
||||||
tag_df = df.copy()
|
"""
|
||||||
tag_df.sort_values(by='edhrecRank', inplace=True)
|
try:
|
||||||
tag_df = tag_df[tag_df['themeTags'].apply(lambda x: tag in x)]
|
# Count existing cards with target tag
|
||||||
# Take top cards based on ideal value
|
print()
|
||||||
pool_size = int(ideal_value * random.randint(2, 3))
|
if not ignore_existing:
|
||||||
tag_df = tag_df.head(pool_size)
|
existing_count = len(self.card_library[self.card_library['Themes'].apply(lambda x: x is not None and tag in x)])
|
||||||
|
remaining_slots = max(0, ideal_value - existing_count + 1)
|
||||||
# Convert to list of card dictionaries
|
|
||||||
card_pool = [
|
|
||||||
{
|
|
||||||
'name': row['name'],
|
|
||||||
'type': row['type'],
|
|
||||||
'manaCost': row['manaCost'],
|
|
||||||
'manaValue': row['manaValue'],
|
|
||||||
'creatureTypes': row['creatureTypes'],
|
|
||||||
'themeTags': row['themeTags']
|
|
||||||
}
|
|
||||||
for _, row in tag_df.iterrows()
|
|
||||||
]
|
|
||||||
|
|
||||||
# Randomly select cards up to ideal value
|
|
||||||
cards_to_add = []
|
|
||||||
while len(cards_to_add) < ideal_value and card_pool:
|
|
||||||
card = random.choice(card_pool)
|
|
||||||
card_pool.remove(card)
|
|
||||||
|
|
||||||
# Check price constraints if enabled
|
|
||||||
if use_scrython and self.set_max_card_price:
|
|
||||||
price = self.price_checker.get_card_price(card['name'])
|
|
||||||
if price > self.max_card_price * 1.1:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Add card if not already in library
|
|
||||||
if card['name'] not in self.card_library['Card Name'].values:
|
|
||||||
if 'Creature' in card['type'] and skip_creatures:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if 'Creature' in card['type']:
|
|
||||||
self.creature_cards += 1
|
|
||||||
skip_creatures = self.creature_cards > self.ideal_creature_count * 1.1
|
|
||||||
cards_to_add.append(card)
|
|
||||||
|
|
||||||
# Add selected cards to library
|
|
||||||
for card in cards_to_add:
|
|
||||||
if len(self.card_library) < 100:
|
|
||||||
self.add_card(card['name'], card['type'],
|
|
||||||
card['manaCost'], card['manaValue'],
|
|
||||||
card['creatureTypes'], card['themeTags'])
|
|
||||||
else:
|
else:
|
||||||
continue
|
existing_count = 0
|
||||||
|
remaining_slots = max(0, ideal_value - existing_count + 1)
|
||||||
|
|
||||||
card_pool_names = [item['name'] for item in card_pool]
|
if remaining_slots == 0:
|
||||||
self.full_df = self.full_df[~self.full_df['name'].isin(card_pool_names)]
|
if not ignore_existing:
|
||||||
self.noncreature_df = self.noncreature_df[~self.noncreature_df['name'].isin(card_pool_names)]
|
logger.info(f'Already have {existing_count} cards with tag "{tag}" - no additional cards needed')
|
||||||
logger.info(f'Added {len(cards_to_add)} {tag} cards')
|
return
|
||||||
#tag_df.to_csv(f'{CSV_DIRECTORY}/test_{tag}.csv', index=False)
|
else:
|
||||||
|
logger.info(f'Already have {ideal_value} cards with tag "{tag}" - no additional cards needed')
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f'Finding {remaining_slots} additional cards with the "{tag}" tag...')
|
||||||
|
|
||||||
|
# Filter cards with the given tag
|
||||||
|
skip_creatures = self.creature_cards > self.ideal_creature_count * 1.1
|
||||||
|
tag_df = df.copy()
|
||||||
|
tag_df.sort_values(by='edhrecRank', inplace=True)
|
||||||
|
tag_df = tag_df[tag_df['themeTags'].apply(lambda x: x is not None and tag in x)]
|
||||||
|
|
||||||
|
# Calculate initial pool size using THEME_POOL_SIZE_MULTIPLIER
|
||||||
|
pool_size = int(remaining_slots * THEME_POOL_SIZE_MULTIPLIER)
|
||||||
|
tag_df = tag_df.head(pool_size)
|
||||||
|
|
||||||
|
# Convert to list of card dictionaries with priority scores
|
||||||
|
card_pool = []
|
||||||
|
for _, row in tag_df.iterrows():
|
||||||
|
theme_tags = row['themeTags'] if row['themeTags'] is not None else []
|
||||||
|
priority = builder_utils.calculate_theme_priority(theme_tags, self.themes, THEME_PRIORITY_BONUS)
|
||||||
|
card_pool.append({
|
||||||
|
'name': row['name'],
|
||||||
|
'type': row['type'],
|
||||||
|
'manaCost': row['manaCost'],
|
||||||
|
'manaValue': row['manaValue'],
|
||||||
|
'creatureTypes': row['creatureTypes'],
|
||||||
|
'themeTags': theme_tags,
|
||||||
|
'priority': priority
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort card pool by priority score
|
||||||
|
card_pool.sort(key=lambda x: x['priority'], reverse=True)
|
||||||
|
|
||||||
|
# Select cards up to remaining slots
|
||||||
|
cards_to_add = []
|
||||||
|
for card in card_pool:
|
||||||
|
if len(cards_to_add) >= remaining_slots:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check price constraints if enabled
|
||||||
|
if use_scrython and hasattr(self, 'max_card_price') and self.max_card_price:
|
||||||
|
price = self.price_checker.get_card_price(card['name'])
|
||||||
|
if price > self.max_card_price * 1.1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle multiple-copy cards
|
||||||
|
if card['name'] in multiple_copy_cards:
|
||||||
|
existing_copies = len(self.card_library[self.card_library['Card Name'] == card['name']])
|
||||||
|
if existing_copies < ideal_value:
|
||||||
|
cards_to_add.append(card)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add new cards if not already in library
|
||||||
|
if card['name'] not in self.card_library['Card Name'].values:
|
||||||
|
if 'Creature' in card['type'] and skip_creatures:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if 'Creature' in card['type']:
|
||||||
|
self.creature_cards += 1
|
||||||
|
skip_creatures = self.creature_cards > self.ideal_creature_count * 1.1
|
||||||
|
cards_to_add.append(card)
|
||||||
|
|
||||||
|
# Add selected cards to library
|
||||||
|
for card in cards_to_add:
|
||||||
|
if len(self.card_library) < 100:
|
||||||
|
self.add_card(card['name'], card['type'],
|
||||||
|
card['manaCost'], card['manaValue'],
|
||||||
|
card['creatureTypes'], card['themeTags'])
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Update DataFrames
|
||||||
|
card_pool_names = [item['name'] for item in card_pool]
|
||||||
|
self.noncreature_df = self.noncreature_df[~self.noncreature_df['name'].isin(card_pool_names)]
|
||||||
|
|
||||||
|
logger.info(f'Added {len(cards_to_add)} {tag} cards (total with tag: {existing_count + len(cards_to_add)})')
|
||||||
|
for card in cards_to_add:
|
||||||
|
print(card['name'])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise ThemeTagError(f"Error processing tag '{tag}'", {"error": str(e)})
|
||||||
|
|
||||||
def add_creatures(self):
|
def add_creatures(self):
|
||||||
"""
|
"""
|
||||||
Add creatures to the deck based on themes and self.weights.
|
Add creatures to the deck based on themes and weights.
|
||||||
|
|
||||||
This method processes the primary, secondary, and tertiary themes to add
|
This method processes the primary, secondary, and tertiary themes to add
|
||||||
creatures proportionally according to their self.weights. The total number of
|
creatures proportionally according to their weights. The total number of
|
||||||
creatures added will approximate the ideal_creature_count.
|
creatures added will approximate the ideal_creature_count.
|
||||||
|
|
||||||
Themes are processed in order of importance (primary -> secondary -> tertiary)
|
The method follows this process:
|
||||||
with error handling to ensure the deck building process continues even if
|
1. Process hidden theme if present
|
||||||
a particular theme encounters issues.
|
2. Process primary theme
|
||||||
|
3. Process secondary theme if present
|
||||||
|
4. Process tertiary theme if present
|
||||||
|
|
||||||
|
Each theme is weighted according to its importance:
|
||||||
|
- Hidden theme: Highest priority if present
|
||||||
|
- Primary theme: Main focus
|
||||||
|
- Secondary theme: Supporting focus
|
||||||
|
- Tertiary theme: Minor focus
|
||||||
|
|
||||||
|
Args:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ThemeWeightingError: If there are issues with theme weight calculations
|
||||||
|
ThemePoolError: If the card pool for a theme is insufficient
|
||||||
|
Exception: For any other unexpected errors during creature addition
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The method uses error handling to ensure the deck building process
|
||||||
|
continues even if a particular theme encounters issues.
|
||||||
"""
|
"""
|
||||||
print(f'Adding creatures to deck based on the ideal creature count of {self.ideal_creature_count}...')
|
print()
|
||||||
|
logger.info(f'Adding creatures to deck based on the ideal creature count of {self.ideal_creature_count}...')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.hidden_theme:
|
if self.hidden_theme:
|
||||||
print(f'Processing Hidden theme: {self.hidden_theme}')
|
print()
|
||||||
|
logger.info(f'Processing Hidden theme: {self.hidden_theme}')
|
||||||
self.weight_by_theme(self.hidden_theme, self.ideal_creature_count, self.hidden_weight, self.creature_df)
|
self.weight_by_theme(self.hidden_theme, self.ideal_creature_count, self.hidden_weight, self.creature_df)
|
||||||
|
|
||||||
print(f'Processing primary theme: {self.primary_theme}')
|
logger.info(f'Processing primary theme: {self.primary_theme}')
|
||||||
self.weight_by_theme(self.primary_theme, self.ideal_creature_count, self.primary_weight, self.creature_df)
|
self.weight_by_theme(self.primary_theme, self.ideal_creature_count, self.primary_weight, self.creature_df)
|
||||||
|
|
||||||
if self.secondary_theme:
|
if self.secondary_theme:
|
||||||
print(f'Processing secondary theme: {self.secondary_theme}')
|
print()
|
||||||
|
logger.info(f'Processing secondary theme: {self.secondary_theme}')
|
||||||
self.weight_by_theme(self.secondary_theme, self.ideal_creature_count, self.secondary_weight, self.creature_df)
|
self.weight_by_theme(self.secondary_theme, self.ideal_creature_count, self.secondary_weight, self.creature_df)
|
||||||
|
|
||||||
if self.tertiary_theme:
|
if self.tertiary_theme:
|
||||||
print(f'Processing tertiary theme: {self.tertiary_theme}')
|
print()
|
||||||
|
logger.info(f'Processing tertiary theme: {self.tertiary_theme}')
|
||||||
self.weight_by_theme(self.tertiary_theme, self.ideal_creature_count, self.tertiary_weight, self.creature_df)
|
self.weight_by_theme(self.tertiary_theme, self.ideal_creature_count, self.tertiary_weight, self.creature_df)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error while adding creatures: {e}")
|
logger.error(f"Error while adding creatures: {e}")
|
||||||
finally:
|
finally:
|
||||||
self.organize_library()
|
self.organize_library()
|
||||||
logger.info(f'Creature addition complete. Total creatures (including commander): {self.creature_cards}')
|
|
||||||
|
|
||||||
def add_ramp(self):
|
def add_ramp(self):
|
||||||
|
"""Add ramp cards to the deck based on ideal ramp count.
|
||||||
|
|
||||||
|
This method adds three categories of ramp cards:
|
||||||
|
1. Mana rocks (artifacts that produce mana) - ~1/3 of ideal ramp count
|
||||||
|
2. Mana dorks (creatures that produce mana) - ~1/4 of ideal ramp count
|
||||||
|
3. General ramp spells - remaining portion of ideal ramp count
|
||||||
|
|
||||||
|
The method uses the add_by_tags() helper to add cards from each category
|
||||||
|
while respecting the deck's themes and color identity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ThemeTagError: If there are issues adding cards with ramp-related tags
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self.add_by_tags('Mana Rock', math.ceil(self.ideal_ramp / 3), self.noncreature_df)
|
self.add_by_tags('Mana Rock', math.ceil(self.ideal_ramp / 3), self.noncreature_df)
|
||||||
self.add_by_tags('Mana Dork', math.ceil(self.ideal_ramp / 4), self.creature_df)
|
self.add_by_tags('Mana Dork', math.ceil(self.ideal_ramp / 4), self.creature_df)
|
||||||
self.add_by_tags('Ramp', math.ceil(self.ideal_ramp / 2), self.noncreature_df)
|
self.add_by_tags('Ramp', self.ideal_ramp, self.noncreature_df)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error while adding Ramp: {e}")
|
logger.error(f"Error while adding Ramp: {e}")
|
||||||
finally:
|
|
||||||
logger.info('Adding Ramp complete.')
|
|
||||||
|
|
||||||
def add_interaction(self):
|
def add_interaction(self):
|
||||||
|
"""Add interaction cards to the deck for removal and protection.
|
||||||
|
|
||||||
|
This method adds two categories of interaction cards:
|
||||||
|
1. Removal spells based on ideal_removal count
|
||||||
|
2. Protection spells based on ideal_protection count
|
||||||
|
|
||||||
|
Cards are selected from non-planeswalker cards to ensure appropriate
|
||||||
|
interaction types are added.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ThemeTagError: If there are issues adding cards with interaction-related tags
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self.add_by_tags('Removal', self.ideal_removal, self.noncreature_nonplaneswaker_df)
|
self.add_by_tags('Removal', self.ideal_removal, self.nonplaneswalker_df)
|
||||||
self.add_by_tags('Protection', self.ideal_protection, self.noncreature_nonplaneswaker_df)
|
self.add_by_tags('Protection', self.ideal_protection, self.nonplaneswalker_df)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error while adding Interaction: {e}")
|
logger.error(f"Error while adding Interaction: {e}")
|
||||||
finally:
|
|
||||||
logger.info('Adding Interaction complete.')
|
|
||||||
|
|
||||||
def add_board_wipes(self):
|
def add_board_wipes(self):
|
||||||
|
"""Add board wipe cards to the deck.
|
||||||
|
|
||||||
|
This method adds board wipe cards based on the ideal_wipes count.
|
||||||
|
Board wipes are selected from the full card pool to include all possible
|
||||||
|
options across different card types.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ThemeTagError: If there are issues adding cards with the 'Board Wipes' tag
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self.add_by_tags('Board Wipes', self.ideal_wipes, self.full_df)
|
self.add_by_tags('Board Wipes', self.ideal_wipes, self.full_df)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error while adding Board Wipes: {e}")
|
logger.error(f"Error while adding Board Wipes: {e}")
|
||||||
finally:
|
|
||||||
logger.info('Adding Board Wipes complete.')
|
|
||||||
|
|
||||||
def add_card_advantage(self):
|
def add_card_advantage(self):
|
||||||
|
"""Add card advantage effects to the deck.
|
||||||
|
|
||||||
|
This method adds two categories of card draw effects:
|
||||||
|
1. Conditional draw effects (20% of ideal_card_advantage)
|
||||||
|
- Cards that draw based on specific conditions or triggers
|
||||||
|
2. Unconditional draw effects (80% of ideal_card_advantage)
|
||||||
|
- Cards that provide straightforward card draw
|
||||||
|
|
||||||
|
Cards are selected from appropriate pools while avoiding planeswalkers
|
||||||
|
for unconditional draw effects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ThemeTagError: If there are issues adding cards with draw-related tags
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self.add_by_tags('Conditional Draw', math.ceil(self.ideal_card_advantage * 0.2), self.full_df)
|
self.add_by_tags('Conditional Draw', math.ceil(self.ideal_card_advantage * 0.2), self.full_df)
|
||||||
self.add_by_tags('Unconditional Draw', math.ceil(self.ideal_card_advantage * 0.8), self.noncreature_nonplaneswaker_df)
|
self.add_by_tags('Unconditional Draw', math.ceil(self.ideal_card_advantage * 0.8), self.nonplaneswalker_df)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error while adding Card Draw: {e}")
|
logger.error(f"Error while adding Card Draw: {e}")
|
||||||
finally:
|
|
||||||
logger.info('Adding Card Draw complete.')
|
|
||||||
|
|
||||||
def fill_out_deck(self):
|
def fill_out_deck(self):
|
||||||
"""Fill out the deck to 100 cards with theme-appropriate cards."""
|
"""Fill out the deck to 100 cards with theme-appropriate cards.
|
||||||
logger.info('Filling out the Library to 100 with cards fitting the themes.')
|
|
||||||
|
|
||||||
|
This method completes the deck by adding remaining cards up to the 100-card
|
||||||
|
requirement, prioritizing cards that match the deck's themes. The process
|
||||||
|
follows these steps:
|
||||||
|
|
||||||
|
1. Calculate how many cards are needed to reach 100
|
||||||
|
2. Add cards from each theme with weighted distribution:
|
||||||
|
- Hidden theme (if present)
|
||||||
|
- Tertiary theme (20% weight if present)
|
||||||
|
- Secondary theme (30% weight if present)
|
||||||
|
- Primary theme (50% weight)
|
||||||
|
|
||||||
|
The method includes safeguards:
|
||||||
|
- Maximum attempts limit to prevent infinite loops
|
||||||
|
- Timeout to prevent excessive runtime
|
||||||
|
- Progress tracking to break early if insufficient progress
|
||||||
|
|
||||||
|
Args:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ThemeTagError: If there are issues adding cards with specific theme tags
|
||||||
|
TimeoutError: If the process exceeds the maximum allowed time
|
||||||
|
|
||||||
|
Note:
|
||||||
|
If the deck cannot be filled to 100 cards, a warning message is logged
|
||||||
|
indicating manual additions may be needed.
|
||||||
|
"""
|
||||||
|
print()
|
||||||
|
logger.info('Filling out the Library to 100 with cards fitting the themes.')
|
||||||
cards_needed = 100 - len(self.card_library)
|
cards_needed = 100 - len(self.card_library)
|
||||||
if cards_needed <= 0:
|
if cards_needed <= 0:
|
||||||
return
|
return
|
||||||
|
|
@ -2274,38 +2433,57 @@ class DeckBuilder:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Add cards from each theme with adjusted self.weights
|
# Add cards from each theme with adjusted self.weights
|
||||||
if self.tertiary_theme:
|
if self.hidden_theme and remaining > 0:
|
||||||
|
self.add_by_tags(self.hidden_theme,
|
||||||
|
math.ceil(weight_multiplier),
|
||||||
|
self.full_df,
|
||||||
|
True)
|
||||||
|
|
||||||
|
# Adjust self.weights based on remaining cards needed
|
||||||
|
remaining = 100 - len(self.card_library)
|
||||||
|
weight_multiplier = remaining / cards_needed
|
||||||
|
if self.tertiary_theme and remaining > 0:
|
||||||
self.add_by_tags(self.tertiary_theme,
|
self.add_by_tags(self.tertiary_theme,
|
||||||
math.ceil(self.tertiary_weight * 10 * weight_multiplier),
|
math.ceil(weight_multiplier * 0.2),
|
||||||
self.noncreature_df)
|
self.noncreature_df,
|
||||||
if self.secondary_theme:
|
True)
|
||||||
|
|
||||||
|
if self.secondary_theme and remaining > 0:
|
||||||
self.add_by_tags(self.secondary_theme,
|
self.add_by_tags(self.secondary_theme,
|
||||||
math.ceil(self.secondary_weight * 3 * weight_multiplier),
|
math.ceil(weight_multiplier * 0.3),
|
||||||
self.noncreature_df)
|
self.noncreature_df,
|
||||||
self.add_by_tags(self.primary_theme,
|
True)
|
||||||
math.ceil(self.primary_weight * 2 * weight_multiplier),
|
if remaining > 0:
|
||||||
self.noncreature_df)
|
self.add_by_tags(self.primary_theme,
|
||||||
|
math.ceil(weight_multiplier * 0.5),
|
||||||
|
self.noncreature_df,
|
||||||
|
True)
|
||||||
|
|
||||||
# Check if we made progress
|
# Check if we made progress
|
||||||
if len(self.card_library) == initial_count:
|
if len(self.card_library) == initial_count:
|
||||||
attempts += 1
|
attempts += 1
|
||||||
if attempts % 5 == 0:
|
if attempts % 5 == 0:
|
||||||
|
print()
|
||||||
logger.warning(f"Made {attempts} attempts, still need {100 - len(self.card_library)} cards")
|
logger.warning(f"Made {attempts} attempts, still need {100 - len(self.card_library)} cards")
|
||||||
|
|
||||||
# Break early if we're stuck
|
# Break early if we're stuck
|
||||||
if attempts >= MAX_ATTEMPTS / 2 and len(self.card_library) < initial_count + (cards_needed / 4):
|
if attempts >= MAX_ATTEMPTS / 2 and len(self.card_library) < initial_count + (cards_needed / 4):
|
||||||
|
print()
|
||||||
logger.warning("Insufficient progress being made, breaking early")
|
logger.warning("Insufficient progress being made, breaking early")
|
||||||
break
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print()
|
||||||
logger.error(f"Error while adding cards: {e}")
|
logger.error(f"Error while adding cards: {e}")
|
||||||
attempts += 1
|
attempts += 1
|
||||||
|
|
||||||
final_count = len(self.card_library)
|
final_count = len(self.card_library)
|
||||||
if final_count < 100:
|
if final_count < 100:
|
||||||
message = f"\nWARNING: Deck is incomplete with {final_count} cards. Manual additions may be needed."
|
message = f"\nWARNING: Deck is incomplete with {final_count} cards. Manual additions may be needed."
|
||||||
|
print()
|
||||||
logger.warning(message)
|
logger.warning(message)
|
||||||
else:
|
else:
|
||||||
|
print()
|
||||||
logger.info(f"Successfully filled deck to {final_count} cards in {attempts} attempts")
|
logger.info(f"Successfully filled deck to {final_count} cards in {attempts} attempts")
|
||||||
def main():
|
def main():
|
||||||
"""Main entry point for deck builder application."""
|
"""Main entry point for deck builder application."""
|
||||||
|
|
|
||||||
107
exceptions.py
107
exceptions.py
|
|
@ -1279,3 +1279,110 @@ class ManaPipError(DeckBuilderError):
|
||||||
code="MANA_PIP_ERR",
|
code="MANA_PIP_ERR",
|
||||||
details=details
|
details=details
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class ThemeTagError(DeckBuilderError):
|
||||||
|
"""Raised when there are issues processing theme tags.
|
||||||
|
|
||||||
|
This exception is used when there are problems processing or validating theme tags,
|
||||||
|
such as invalid tag formats, missing required tags, or tag validation failures.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> raise ThemeTagError(
|
||||||
|
... "Invalid theme tag format",
|
||||||
|
... {"tag": "invalid#tag", "expected_format": "theme:subtheme"}
|
||||||
|
... )
|
||||||
|
|
||||||
|
>>> raise ThemeTagError(
|
||||||
|
... "Missing required theme tags",
|
||||||
|
... {"card_name": "Example Card", "required_tags": ["theme:tribal", "theme:synergy"]}
|
||||||
|
... )
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message: str, details: dict | None = None):
|
||||||
|
"""Initialize theme tag error.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Description of the theme tag processing failure
|
||||||
|
details: Additional context about the error
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
message,
|
||||||
|
code="THEME_TAG_ERR",
|
||||||
|
details=details
|
||||||
|
)
|
||||||
|
|
||||||
|
class ThemeWeightingError(DeckBuilderError):
|
||||||
|
"""Raised when there are issues with theme-based card weighting.
|
||||||
|
|
||||||
|
This exception is used when there are problems calculating or validating
|
||||||
|
theme weights, such as invalid weight values, calculation errors, or
|
||||||
|
inconsistencies in theme weight distribution.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> raise ThemeWeightingError(
|
||||||
|
... "Invalid theme weight value",
|
||||||
|
... {"theme": "tribal", "weight": -1, "valid_range": "0-100"}
|
||||||
|
... )
|
||||||
|
|
||||||
|
>>> raise ThemeWeightingError(
|
||||||
|
... "Theme weight calculation error",
|
||||||
|
... {"theme": "artifacts", "error": "Division by zero in weight normalization"}
|
||||||
|
... )
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message: str, details: dict | None = None):
|
||||||
|
"""Initialize theme weighting error.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Description of the theme weighting failure
|
||||||
|
details: Additional context about the error
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
message,
|
||||||
|
code="THEME_WEIGHT_ERR",
|
||||||
|
details=details
|
||||||
|
)
|
||||||
|
|
||||||
|
class ThemePoolError(DeckBuilderError):
|
||||||
|
"""Raised when there are issues with the theme card pool.
|
||||||
|
|
||||||
|
This exception is used when there are problems creating or managing the theme
|
||||||
|
card pool, such as empty pools, insufficient cards, or invalid pool configurations.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> raise ThemePoolError(
|
||||||
|
... "Empty theme card pool",
|
||||||
|
... {"theme": "spellslinger", "required_cards": 30}
|
||||||
|
... )
|
||||||
|
|
||||||
|
>>> raise ThemePoolError(
|
||||||
|
... "Insufficient cards in theme pool",
|
||||||
|
... {
|
||||||
|
... "theme": "artifacts",
|
||||||
|
... "available_cards": 15,
|
||||||
|
... "required_cards": 25
|
||||||
|
... }
|
||||||
|
... )
|
||||||
|
|
||||||
|
>>> raise ThemePoolError(
|
||||||
|
... "Invalid card pool configuration",
|
||||||
|
... {
|
||||||
|
... "theme": "tribal",
|
||||||
|
... "creature_type": "Dragon",
|
||||||
|
... "error": "No cards match creature type"
|
||||||
|
... }
|
||||||
|
... )
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message: str, details: dict | None = None):
|
||||||
|
"""Initialize theme pool error.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Description of the theme pool failure
|
||||||
|
details: Additional context about the error
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
message,
|
||||||
|
code="THEME_POOL_ERR",
|
||||||
|
details=details
|
||||||
|
)
|
||||||
14
settings.py
14
settings.py
|
|
@ -544,7 +544,7 @@ REMOVAL_TEXT_PATTERNS = [
|
||||||
'returns target.*to.*hand'
|
'returns target.*to.*hand'
|
||||||
]
|
]
|
||||||
|
|
||||||
REMOVAL_SPECIFIC_CARDS = [] # type: list
|
REMOVAL_SPECIFIC_CARDS = ['from.*graveyard.*hand'] # type: list
|
||||||
|
|
||||||
REMOVAL_EXCLUSION_PATTERNS = [] # type: list
|
REMOVAL_EXCLUSION_PATTERNS = [] # type: list
|
||||||
|
|
||||||
|
|
@ -1032,7 +1032,17 @@ REQUIRED_COLUMNS: List[str] = [
|
||||||
'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side'
|
'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side'
|
||||||
]
|
]
|
||||||
|
|
||||||
# Constants for theme weight management
|
# Constants for theme weight management and selection
|
||||||
|
|
||||||
|
# Multiplier for initial card pool size during theme-based selection
|
||||||
|
THEME_POOL_SIZE_MULTIPLIER: Final[float] = 2.0
|
||||||
|
|
||||||
|
# Bonus multiplier for cards that match multiple deck themes
|
||||||
|
THEME_PRIORITY_BONUS: Final[float] = 1.2
|
||||||
|
|
||||||
|
# Safety multiplier to avoid overshooting target counts
|
||||||
|
THEME_WEIGHT_MULTIPLIER: Final[float] = 0.9
|
||||||
|
|
||||||
THEME_WEIGHTS_DEFAULT: Final[Dict[str, float]] = {
|
THEME_WEIGHTS_DEFAULT: Final[Dict[str, float]] = {
|
||||||
'primary': 1.0,
|
'primary': 1.0,
|
||||||
'secondary': 0.6,
|
'secondary': 0.6,
|
||||||
|
|
|
||||||
39
setup.py
39
setup.py
|
|
@ -1,26 +1,53 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
"""MTG Python Deckbuilder setup module.
|
||||||
|
|
||||||
|
This module provides the main setup functionality for the MTG Python Deckbuilder
|
||||||
|
application. It handles initial setup tasks such as downloading card data,
|
||||||
|
creating color-filtered card lists, and generating commander-eligible card lists.
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Initial setup and configuration
|
||||||
|
- Card data download and processing
|
||||||
|
- Color-based card filtering
|
||||||
|
- Commander card list generation
|
||||||
|
- CSV file management and validation
|
||||||
|
|
||||||
|
The module works in conjunction with setup_utils.py for utility functions and
|
||||||
|
exceptions.py for error handling.
|
||||||
|
"""
|
||||||
|
|
||||||
# Standard library imports
|
# Standard library imports
|
||||||
import logging
|
import logging
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from typing import Union, List, Dict, Any
|
from typing import Union, List, Dict, Any
|
||||||
|
|
||||||
# Third-party imports
|
# Third-party imports
|
||||||
import pandas as pd
|
|
||||||
import inquirer
|
import inquirer
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
# Local application imports
|
# Local application imports
|
||||||
from settings import (
|
from settings import (
|
||||||
banned_cards, CSV_DIRECTORY, SETUP_COLORS, COLOR_ABRV, MTGJSON_API_URL
|
banned_cards,
|
||||||
|
COLOR_ABRV,
|
||||||
|
CSV_DIRECTORY,
|
||||||
|
MTGJSON_API_URL,
|
||||||
|
SETUP_COLORS
|
||||||
)
|
)
|
||||||
from setup_utils import (
|
from setup_utils import (
|
||||||
download_cards_csv, filter_dataframe, process_legendary_cards, filter_by_color_identity
|
download_cards_csv,
|
||||||
|
filter_by_color_identity,
|
||||||
|
filter_dataframe,
|
||||||
|
process_legendary_cards
|
||||||
)
|
)
|
||||||
from exceptions import (
|
from exceptions import (
|
||||||
CSVFileNotFoundError, MTGJSONDownloadError, DataFrameProcessingError,
|
CSVFileNotFoundError,
|
||||||
ColorFilterError, CommanderValidationError
|
ColorFilterError,
|
||||||
|
CommanderValidationError,
|
||||||
|
DataFrameProcessingError,
|
||||||
|
MTGJSONDownloadError
|
||||||
)
|
)
|
||||||
# Create logs directory if it doesn't exist
|
# Create logs directory if it doesn't exist
|
||||||
if not os.path.exists('logs'):
|
if not os.path.exists('logs'):
|
||||||
|
|
|
||||||
|
|
@ -347,7 +347,7 @@ def process_legendary_cards(df: pd.DataFrame) -> pd.DataFrame:
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
def process_card_dataframe(df: CardLibraryDF, batch_size: int = 1000, columns_to_keep: Optional[List[str]] = None,
|
def process_card_dataframe(df: CardLibraryDF, batch_size: int = 1000, columns_to_keep: Optional[List[str]] = None,
|
||||||
include_commander_cols: bool = False, skip_availability_checks: bool = False) -> pd.DataFrame:
|
include_commander_cols: bool = False, skip_availability_checks: bool = False) -> CardLibraryDF:
|
||||||
"""Process DataFrame with common operations in batches.
|
"""Process DataFrame with common operations in batches.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
|
||||||
44
tagger.py
44
tagger.py
|
|
@ -293,7 +293,7 @@ def create_theme_tags(df: pd.DataFrame, color: str) -> None:
|
||||||
raise TypeError("df must be a pandas DataFrame")
|
raise TypeError("df must be a pandas DataFrame")
|
||||||
if not isinstance(color, str):
|
if not isinstance(color, str):
|
||||||
raise TypeError("color must be a string")
|
raise TypeError("color must be a string")
|
||||||
if color not in settings.colors:
|
if color not in settings.COLORS:
|
||||||
raise ValueError(f"Invalid color: {color}")
|
raise ValueError(f"Invalid color: {color}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -2633,6 +2633,24 @@ def create_token_modifier_mask(df: pd.DataFrame) -> pd.Series:
|
||||||
|
|
||||||
return has_modifier & has_effect & ~name_exclusions
|
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:
|
def tag_for_tokens(df: pd.DataFrame, color: str) -> None:
|
||||||
"""Tag cards that create or modify tokens using vectorized operations.
|
"""Tag cards that create or modify tokens using vectorized operations.
|
||||||
|
|
||||||
|
|
@ -2671,6 +2689,13 @@ def tag_for_tokens(df: pd.DataFrame, color: str) -> None:
|
||||||
['Token Modification', 'Token Creation', 'Tokens Matter'])
|
['Token Modification', 'Token Creation', 'Tokens Matter'])
|
||||||
logger.info('Tagged %d cards that modify token creation', modifier_mask.sum())
|
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()
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
||||||
logger.info('Completed token tagging in %.2fs', duration)
|
logger.info('Completed token tagging in %.2fs', duration)
|
||||||
|
|
||||||
|
|
@ -3638,7 +3663,7 @@ def tag_for_cantrips(df: pd.DataFrame, color: str) -> None:
|
||||||
logger.error('Error tagging Cantrips in %s_cards.csv: %s', color, str(e))
|
logger.error('Error tagging Cantrips in %s_cards.csv: %s', color, str(e))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
## Magecraft
|
||||||
def create_magecraft_mask(df: pd.DataFrame) -> pd.Series:
|
def create_magecraft_mask(df: pd.DataFrame) -> pd.Series:
|
||||||
"""Create a boolean mask for cards with magecraft effects.
|
"""Create a boolean mask for cards with magecraft effects.
|
||||||
|
|
||||||
|
|
@ -6369,6 +6394,18 @@ def create_removal_text_mask(df: pd.DataFrame) -> pd.Series:
|
||||||
"""
|
"""
|
||||||
return tag_utils.create_text_mask(df, settings.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:
|
def tag_for_removal(df: pd.DataFrame, color: str) -> None:
|
||||||
"""Tag cards that provide spot removal using vectorized operations.
|
"""Tag cards that provide spot removal using vectorized operations.
|
||||||
|
|
||||||
|
|
@ -6422,7 +6459,8 @@ def tag_for_removal(df: pd.DataFrame, color: str) -> None:
|
||||||
|
|
||||||
def run_tagging():
|
def run_tagging():
|
||||||
start_time = pd.Timestamp.now()
|
start_time = pd.Timestamp.now()
|
||||||
for color in settings.colors:
|
for color in settings.COLORS:
|
||||||
load_dataframe(color)
|
load_dataframe(color)
|
||||||
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
duration = (pd.Timestamp.now() - start_time).total_seconds()
|
||||||
logger.info(f'Tagged cards in {duration:.2f}s')
|
logger.info(f'Tagged cards in {duration:.2f}s')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,5 @@ NonCreatureDF = pd.DataFrame
|
||||||
EnchantmentDF = pd.DataFrame
|
EnchantmentDF = pd.DataFrame
|
||||||
InstantDF = pd.DataFrame
|
InstantDF = pd.DataFrame
|
||||||
PlaneswalkerDF = pd.DataFrame
|
PlaneswalkerDF = pd.DataFrame
|
||||||
|
NonPlaneswalkerDF = pd.DataFrame
|
||||||
SorceryDF = pd.DataFrame
|
SorceryDF = pd.DataFrame
|
||||||
Loading…
Add table
Add a link
Reference in a new issue