mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-22 04:50:46 +02:00
Started refactoring up through the add_dual_lands function in deck_builder
This commit is contained in:
parent
319f7848d3
commit
47c2cee00f
5 changed files with 1970 additions and 640 deletions
679
builder_utils.py
679
builder_utils.py
|
@ -1,4 +1,7 @@
|
|||
from typing import Dict, List, Tuple, Optional, Any, Callable, TypeVar, Union
|
||||
from typing import Dict, List, Tuple, Optional, Any, Callable, TypeVar, Union, cast
|
||||
import pandas as pd
|
||||
from price_check import PriceChecker
|
||||
from input_handler import InputHandler
|
||||
import logging
|
||||
import functools
|
||||
import time
|
||||
|
@ -13,14 +16,39 @@ from settings import (
|
|||
DATAFRAME_VALIDATION_TIMEOUT,
|
||||
DATAFRAME_BATCH_SIZE,
|
||||
DATAFRAME_TRANSFORM_TIMEOUT,
|
||||
DATAFRAME_REQUIRED_COLUMNS
|
||||
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,
|
||||
)
|
||||
from exceptions import (
|
||||
DeckBuilderError,
|
||||
DuplicateCardError,
|
||||
CSVValidationError,
|
||||
DataFrameValidationError,
|
||||
DataFrameTimeoutError,
|
||||
EmptyDataFrameError
|
||||
EmptyDataFrameError,
|
||||
FetchLandSelectionError,
|
||||
FetchLandValidationError,
|
||||
KindredLandSelectionError,
|
||||
KindredLandValidationError,
|
||||
ThemeSelectionError,
|
||||
ThemeWeightError,
|
||||
CardTypeCountError
|
||||
)
|
||||
|
||||
logging.basicConfig(
|
||||
|
@ -329,3 +357,648 @@ def validate_commander_selection(df: pd.DataFrame, commander_name: str) -> Dict:
|
|||
except Exception as e:
|
||||
logger.error(f"Error validating commander selection: {e}")
|
||||
raise DeckBuilderError(f"Failed to validate commander selection: {str(e)}")
|
||||
|
||||
def select_theme(themes_list: List[str], prompt: str, optional=False) -> str:
|
||||
"""Handle the selection of a theme from a list with user interaction.
|
||||
|
||||
Args:
|
||||
themes_list: List of available themes to choose from
|
||||
prompt: Message to display when prompting for theme selection
|
||||
|
||||
Returns:
|
||||
str: Selected theme name
|
||||
|
||||
Raises:
|
||||
ThemeSelectionError: If user chooses to stop without selecting a theme
|
||||
"""
|
||||
try:
|
||||
if not themes_list:
|
||||
raise ThemeSelectionError("No themes available for selection")
|
||||
|
||||
print(prompt)
|
||||
for idx, theme in enumerate(themes_list, 1):
|
||||
print(f"{idx}. {theme}")
|
||||
print("0. Stop selection")
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice = int(input("Enter the number of your choice: "))
|
||||
if choice == 0:
|
||||
return 'Stop Here'
|
||||
if 1 <= choice <= len(themes_list):
|
||||
return themes_list[choice - 1]
|
||||
print("Invalid choice. Please try again.")
|
||||
except ValueError:
|
||||
print("Please enter a valid number.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in theme selection: {e}")
|
||||
raise ThemeSelectionError(f"Theme selection failed: {str(e)}")
|
||||
|
||||
def adjust_theme_weights(primary_theme: str,
|
||||
secondary_theme: str,
|
||||
tertiary_theme: str,
|
||||
weights: Dict[str, float]) -> Dict[str, float]:
|
||||
"""Calculate adjusted theme weights based on theme combinations.
|
||||
|
||||
Args:
|
||||
primary_theme: The main theme selected
|
||||
secondary_theme: The second theme selected
|
||||
tertiary_theme: The third theme selected
|
||||
weights: Initial theme weights dictionary
|
||||
|
||||
Returns:
|
||||
Dict[str, float]: Adjusted theme weights
|
||||
|
||||
Raises:
|
||||
ThemeWeightError: If weight calculations fail
|
||||
"""
|
||||
try:
|
||||
adjusted_weights = weights.copy()
|
||||
|
||||
for theme, factors in WEIGHT_ADJUSTMENT_FACTORS.items():
|
||||
if theme in [primary_theme, secondary_theme, tertiary_theme]:
|
||||
for target_theme, factor in factors.items():
|
||||
if target_theme in adjusted_weights:
|
||||
adjusted_weights[target_theme] = round(adjusted_weights[target_theme] * factor, 2)
|
||||
|
||||
# Normalize weights to ensure they sum to 1.0
|
||||
total_weight = sum(adjusted_weights.values())
|
||||
if total_weight > 0:
|
||||
adjusted_weights = {k: round(v/total_weight, 2) for k, v in adjusted_weights.items()}
|
||||
|
||||
print(adjusted_weights)
|
||||
return adjusted_weights
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adjusting theme weights: {e}")
|
||||
raise ThemeWeightError(f"Failed to adjust theme weights: {str(e)}")
|
||||
def configure_price_settings(price_checker: Optional[PriceChecker], input_handler: InputHandler) -> None:
|
||||
"""Handle configuration of price settings if price checking is enabled.
|
||||
|
||||
Args:
|
||||
price_checker: Optional PriceChecker instance for price validation
|
||||
input_handler: InputHandler instance for user input
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
ValueError: If invalid price values are provided
|
||||
"""
|
||||
if not price_checker:
|
||||
return
|
||||
|
||||
try:
|
||||
# Configure max deck price
|
||||
print('Would you like to set an intended max price of the deck?\n'
|
||||
'There will be some leeway of ~10%, with a couple alternative options provided.')
|
||||
if input_handler.questionnaire('Confirm', message='', default_value=False):
|
||||
print('What would you like the max price to be?')
|
||||
max_deck_price = float(input_handler.questionnaire('Number', default_value=DEFAULT_MAX_DECK_PRICE))
|
||||
price_checker.max_deck_price = max_deck_price
|
||||
print()
|
||||
|
||||
# Configure max card price
|
||||
print('Would you like to set a max price per card?\n'
|
||||
'There will be some leeway of ~10% when choosing cards and you can choose to keep it or not.')
|
||||
if input_handler.questionnaire('Confirm', message='', default_value=False):
|
||||
print('What would you like the max price to be?')
|
||||
max_card_price = float(input_handler.questionnaire('Number', default_value=DEFAULT_MAX_CARD_PRICE))
|
||||
price_checker.max_card_price = max_card_price
|
||||
print()
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Error configuring price settings: {e}")
|
||||
raise
|
||||
|
||||
def get_deck_composition_values(input_handler: InputHandler) -> Dict[str, int]:
|
||||
"""Collect deck composition values from the user.
|
||||
|
||||
Args:
|
||||
input_handler: InputHandler instance for user input
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: Mapping of component names to their values
|
||||
|
||||
Raises:
|
||||
ValueError: If invalid numeric values are provided
|
||||
"""
|
||||
try:
|
||||
composition = {}
|
||||
for component, prompt in DECK_COMPOSITION_PROMPTS.items():
|
||||
if component not in ['max_deck_price', 'max_card_price']:
|
||||
default_map = {
|
||||
'ramp': DEFAULT_RAMP_COUNT,
|
||||
'lands': DEFAULT_LAND_COUNT,
|
||||
'basic_lands': DEFAULT_BASIC_LAND_COUNT,
|
||||
'creatures': DEFAULT_CREATURE_COUNT,
|
||||
'removal': DEFAULT_REMOVAL_COUNT,
|
||||
'wipes': DEFAULT_WIPES_COUNT,
|
||||
'card_advantage': DEFAULT_CARD_ADVANTAGE_COUNT,
|
||||
'protection': DEFAULT_PROTECTION_COUNT
|
||||
}
|
||||
default_value = default_map.get(component, 0)
|
||||
|
||||
print(prompt)
|
||||
composition[component] = int(input_handler.questionnaire('Number', message='Default', default_value=default_value))
|
||||
print()
|
||||
|
||||
return composition
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Error getting deck composition values: {e}")
|
||||
raise
|
||||
|
||||
def assign_sort_order(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Assign sort order to cards based on their types.
|
||||
|
||||
This function adds a 'Sort Order' column to the DataFrame based on the
|
||||
CARD_TYPE_SORT_ORDER constant from settings. Cards are sorted according to
|
||||
their primary type, with the order specified in CARD_TYPE_SORT_ORDER.
|
||||
|
||||
Args:
|
||||
df: DataFrame containing card information with a 'Card Type' column
|
||||
|
||||
Returns:
|
||||
DataFrame with an additional 'Sort Order' column
|
||||
|
||||
Example:
|
||||
>>> df = pd.DataFrame({
|
||||
... 'Card Type': ['Creature', 'Instant', 'Land']
|
||||
... })
|
||||
>>> sorted_df = assign_sort_order(df)
|
||||
>>> sorted_df['Sort Order'].tolist()
|
||||
['Creature', 'Instant', 'Land']
|
||||
"""
|
||||
# Create a copy of the input DataFrame
|
||||
df = df.copy()
|
||||
|
||||
# Initialize Sort Order column with default value
|
||||
df['Sort Order'] = 'Other'
|
||||
|
||||
# Assign sort order based on card types
|
||||
for card_type in CARD_TYPE_SORT_ORDER:
|
||||
mask = df['Card Type'].str.contains(card_type, case=False, na=False)
|
||||
df.loc[mask, 'Sort Order'] = card_type
|
||||
|
||||
# Convert Sort Order to categorical for proper sorting
|
||||
df['Sort Order'] = pd.Categorical(
|
||||
df['Sort Order'],
|
||||
categories=CARD_TYPE_SORT_ORDER + ['Other'],
|
||||
ordered=True
|
||||
)
|
||||
return df
|
||||
|
||||
def process_duplicate_cards(card_library: pd.DataFrame, duplicate_lists: List[str]) -> pd.DataFrame:
|
||||
"""Process duplicate cards in the library and consolidate them with updated counts.
|
||||
|
||||
This function identifies duplicate cards that are allowed to have multiple copies
|
||||
(like basic lands and certain special cards), consolidates them into single entries,
|
||||
and updates their counts. Card names are formatted using DUPLICATE_CARD_FORMAT.
|
||||
|
||||
Args:
|
||||
card_library: DataFrame containing the deck's card library
|
||||
duplicate_lists: List of card names allowed to have multiple copies
|
||||
|
||||
Returns:
|
||||
DataFrame with processed duplicate cards and updated counts
|
||||
|
||||
Raises:
|
||||
DuplicateCardError: If there are issues processing duplicate cards
|
||||
|
||||
Example:
|
||||
>>> card_library = pd.DataFrame({
|
||||
... 'name': ['Forest', 'Forest', 'Mountain', 'Mountain', 'Sol Ring'],
|
||||
... 'type': ['Basic Land', 'Basic Land', 'Basic Land', 'Basic Land', 'Artifact']
|
||||
... })
|
||||
>>> duplicate_lists = ['Forest', 'Mountain']
|
||||
>>> result = process_duplicate_cards(card_library, duplicate_lists)
|
||||
>>> print(result['name'].tolist())
|
||||
['Forest x 2', 'Mountain x 2', 'Sol Ring']
|
||||
"""
|
||||
try:
|
||||
# Create a copy of the input DataFrame
|
||||
processed_library = card_library.copy()
|
||||
|
||||
# Process each allowed duplicate card
|
||||
for card_name in duplicate_lists:
|
||||
# Find all instances of the card
|
||||
card_mask = processed_library['name'] == card_name
|
||||
card_count = card_mask.sum()
|
||||
|
||||
if card_count > 1:
|
||||
# Keep only the first instance and update its name with count
|
||||
first_instance = processed_library[card_mask].iloc[0]
|
||||
processed_library = processed_library[~card_mask]
|
||||
|
||||
first_instance['name'] = DUPLICATE_CARD_FORMAT.format(
|
||||
card_name=card_name,
|
||||
count=card_count
|
||||
)
|
||||
processed_library = pd.concat([processed_library, pd.DataFrame([first_instance])])
|
||||
|
||||
return processed_library.reset_index(drop=True)
|
||||
|
||||
except Exception as e:
|
||||
raise DuplicateCardError(
|
||||
f"Failed to process duplicate cards: {str(e)}",
|
||||
details={'error': str(e)}
|
||||
)
|
||||
|
||||
def count_cards_by_type(card_library: pd.DataFrame, card_types: List[str]) -> Dict[str, int]:
|
||||
"""Count the number of cards for each specified card type in the library.
|
||||
|
||||
Args:
|
||||
card_library: DataFrame containing the card library
|
||||
card_types: List of card types to count
|
||||
|
||||
Returns:
|
||||
Dictionary mapping card types to their counts
|
||||
|
||||
Raises:
|
||||
CardTypeCountError: If counting fails for any card type
|
||||
"""
|
||||
try:
|
||||
type_counts = {}
|
||||
for card_type in card_types:
|
||||
# Use pandas str.contains() for efficient type matching
|
||||
# Case-insensitive matching with na=False to handle missing values
|
||||
type_mask = card_library['type'].str.contains(
|
||||
card_type,
|
||||
case=False,
|
||||
na=False
|
||||
)
|
||||
type_counts[card_type] = int(type_mask.sum())
|
||||
|
||||
return type_counts
|
||||
except Exception as e:
|
||||
logger.error(f"Error counting cards by type: {e}")
|
||||
raise CardTypeCountError(f"Failed to count cards by type: {str(e)}")
|
||||
|
||||
def calculate_basics_per_color(total_basics: int, num_colors: int) -> Tuple[int, int]:
|
||||
"""Calculate the number of basic lands per color and remaining basics.
|
||||
|
||||
Args:
|
||||
total_basics: Total number of basic lands to distribute
|
||||
num_colors: Number of colors in the deck
|
||||
|
||||
Returns:
|
||||
Tuple containing (basics per color, remaining basics)
|
||||
|
||||
Example:
|
||||
>>> calculate_basics_per_color(20, 3)
|
||||
(6, 2) # 6 basics per color with 2 remaining
|
||||
"""
|
||||
if num_colors == 0:
|
||||
return 0, total_basics
|
||||
|
||||
basics_per_color = total_basics // num_colors
|
||||
remaining_basics = total_basics % num_colors
|
||||
|
||||
return basics_per_color, remaining_basics
|
||||
|
||||
def get_basic_land_mapping(use_snow_covered: bool = False) -> Dict[str, str]:
|
||||
"""Get the appropriate basic land mapping based on snow-covered preference.
|
||||
|
||||
Args:
|
||||
use_snow_covered: Whether to use snow-covered basic lands
|
||||
|
||||
Returns:
|
||||
Dictionary mapping colors to their corresponding basic land names
|
||||
|
||||
Example:
|
||||
>>> get_basic_land_mapping(False)
|
||||
{'W': 'Plains', 'U': 'Island', ...}
|
||||
>>> get_basic_land_mapping(True)
|
||||
{'W': 'Snow-Covered Plains', 'U': 'Snow-Covered Island', ...}
|
||||
"""
|
||||
return SNOW_BASIC_LAND_MAPPING if use_snow_covered else COLOR_TO_BASIC_LAND
|
||||
|
||||
def distribute_remaining_basics(
|
||||
basics_per_color: Dict[str, int],
|
||||
remaining_basics: int,
|
||||
colors: List[str]
|
||||
) -> Dict[str, int]:
|
||||
"""Distribute remaining basic lands across colors.
|
||||
|
||||
This function takes the initial distribution of basic lands and distributes
|
||||
any remaining basics across the colors. The distribution prioritizes colors
|
||||
based on their position in the color list (typically WUBRG order).
|
||||
|
||||
Args:
|
||||
basics_per_color: Initial distribution of basics per color
|
||||
remaining_basics: Number of remaining basics to distribute
|
||||
colors: List of colors to distribute basics across
|
||||
|
||||
Returns:
|
||||
Updated dictionary with final basic land counts per color
|
||||
|
||||
Example:
|
||||
>>> distribute_remaining_basics(
|
||||
... {'W': 6, 'U': 6, 'B': 6},
|
||||
... 2,
|
||||
... ['W', 'U', 'B']
|
||||
... )
|
||||
{'W': 7, 'U': 7, 'B': 6}
|
||||
"""
|
||||
if not colors:
|
||||
return basics_per_color
|
||||
|
||||
# Create a copy to avoid modifying the input dictionary
|
||||
final_distribution = basics_per_color.copy()
|
||||
|
||||
# Distribute remaining basics
|
||||
color_index = 0
|
||||
while remaining_basics > 0 and color_index < len(colors):
|
||||
color = colors[color_index]
|
||||
if color in final_distribution:
|
||||
final_distribution[color] += 1
|
||||
remaining_basics -= 1
|
||||
color_index = (color_index + 1) % len(colors)
|
||||
|
||||
return final_distribution
|
||||
|
||||
def validate_staple_land_conditions(
|
||||
land_name: str,
|
||||
conditions: dict,
|
||||
commander_tags: List[str],
|
||||
colors: List[str],
|
||||
commander_power: int
|
||||
) -> bool:
|
||||
"""Validate if a staple land meets its inclusion conditions.
|
||||
|
||||
Args:
|
||||
land_name: Name of the staple land to validate
|
||||
conditions: Dictionary mapping land names to their condition functions
|
||||
commander_tags: List of tags associated with the commander
|
||||
colors: List of colors in the deck
|
||||
commander_power: Power level of the commander
|
||||
|
||||
Returns:
|
||||
bool: True if the land meets its conditions, False otherwise
|
||||
|
||||
Example:
|
||||
>>> conditions = {'Command Tower': lambda tags, colors, power: len(colors) > 1}
|
||||
>>> validate_staple_land_conditions('Command Tower', conditions, [], ['W', 'U'], 7)
|
||||
True
|
||||
"""
|
||||
condition = conditions.get(land_name)
|
||||
if not condition:
|
||||
return False
|
||||
return condition(commander_tags, colors, commander_power)
|
||||
|
||||
def process_staple_lands(
|
||||
lands_to_add: List[str],
|
||||
card_library: pd.DataFrame,
|
||||
land_df: pd.DataFrame
|
||||
) -> pd.DataFrame:
|
||||
"""Update the land DataFrame by removing added staple lands.
|
||||
|
||||
Args:
|
||||
lands_to_add: List of staple land names to be added
|
||||
card_library: DataFrame containing all available cards
|
||||
land_df: DataFrame containing available lands
|
||||
|
||||
Returns:
|
||||
Updated land DataFrame with staple lands removed
|
||||
|
||||
Example:
|
||||
>>> process_staple_lands(['Command Tower'], card_library, land_df)
|
||||
DataFrame without 'Command Tower' in the available lands
|
||||
"""
|
||||
updated_land_df = land_df[~land_df['name'].isin(lands_to_add)]
|
||||
return updated_land_df
|
||||
|
||||
def validate_fetch_land_count(count: int, min_count: int = 0, max_count: int = 9) -> int:
|
||||
"""Validate the requested number of fetch lands.
|
||||
|
||||
Args:
|
||||
count: Number of fetch lands requested
|
||||
min_count: Minimum allowed fetch lands (default: 0)
|
||||
max_count: Maximum allowed fetch lands (default: 9)
|
||||
|
||||
Returns:
|
||||
Validated fetch land count
|
||||
|
||||
Raises:
|
||||
FetchLandValidationError: If count is invalid
|
||||
|
||||
Example:
|
||||
>>> validate_fetch_land_count(5)
|
||||
5
|
||||
>>> validate_fetch_land_count(-1) # raises FetchLandValidationError
|
||||
"""
|
||||
try:
|
||||
fetch_count = int(count)
|
||||
if fetch_count < min_count or fetch_count > max_count:
|
||||
raise FetchLandValidationError(
|
||||
f"Fetch land count must be between {min_count} and {max_count}",
|
||||
{"requested": fetch_count, "min": min_count, "max": max_count}
|
||||
)
|
||||
return fetch_count
|
||||
except ValueError:
|
||||
raise FetchLandValidationError(
|
||||
f"Invalid fetch land count: {count}",
|
||||
{"value": count}
|
||||
)
|
||||
|
||||
def get_available_fetch_lands(colors: List[str], price_checker: Optional[Any] = None,
|
||||
max_price: Optional[float] = None) -> List[str]:
|
||||
"""Get list of fetch lands available for the deck's colors and budget.
|
||||
|
||||
Args:
|
||||
colors: List of deck colors
|
||||
price_checker: Optional price checker instance
|
||||
max_price: Maximum allowed price per card
|
||||
|
||||
Returns:
|
||||
List of available fetch land names
|
||||
|
||||
Example:
|
||||
>>> get_available_fetch_lands(['U', 'R'])
|
||||
['Scalding Tarn', 'Flooded Strand', ...]
|
||||
"""
|
||||
from settings import GENERIC_FETCH_LANDS, COLOR_TO_FETCH_LANDS
|
||||
|
||||
# Start with generic fetches that work in any deck
|
||||
available_fetches = GENERIC_FETCH_LANDS.copy()
|
||||
|
||||
# Add color-specific fetches
|
||||
for color in colors:
|
||||
if color in COLOR_TO_FETCH_LANDS:
|
||||
available_fetches.extend(COLOR_TO_FETCH_LANDS[color])
|
||||
|
||||
# Remove duplicates while preserving order
|
||||
available_fetches = list(dict.fromkeys(available_fetches))
|
||||
|
||||
# Filter by price if price checking is enabled
|
||||
if price_checker and max_price:
|
||||
available_fetches = [
|
||||
fetch for fetch in available_fetches
|
||||
if price_checker.get_card_price(fetch) <= max_price * 1.1
|
||||
]
|
||||
|
||||
return available_fetches
|
||||
|
||||
def select_fetch_lands(available_fetches: List[str], count: int,
|
||||
allow_duplicates: bool = False) -> List[str]:
|
||||
"""Randomly select fetch lands from the available pool.
|
||||
|
||||
Args:
|
||||
available_fetches: List of available fetch lands
|
||||
count: Number of fetch lands to select
|
||||
allow_duplicates: Whether to allow duplicate selections
|
||||
|
||||
Returns:
|
||||
List of selected fetch land names
|
||||
|
||||
Raises:
|
||||
FetchLandSelectionError: If unable to select required number of fetches
|
||||
|
||||
Example:
|
||||
>>> select_fetch_lands(['Flooded Strand', 'Polluted Delta'], 2)
|
||||
['Polluted Delta', 'Flooded Strand']
|
||||
"""
|
||||
import random
|
||||
|
||||
if not available_fetches:
|
||||
raise FetchLandSelectionError(
|
||||
"No fetch lands available to select from",
|
||||
{"requested": count}
|
||||
)
|
||||
|
||||
if not allow_duplicates and count > len(available_fetches):
|
||||
raise FetchLandSelectionError(
|
||||
f"Not enough unique fetch lands available (requested {count}, have {len(available_fetches)})",
|
||||
{"requested": count, "available": len(available_fetches)}
|
||||
)
|
||||
|
||||
if allow_duplicates:
|
||||
return random.choices(available_fetches, k=count)
|
||||
else:
|
||||
return random.sample(available_fetches, k=count)
|
||||
|
||||
def validate_kindred_lands(land_name: str, commander_tags: List[str], colors: List[str]) -> bool:
|
||||
"""Validate if a Kindred land meets inclusion criteria.
|
||||
|
||||
Args:
|
||||
land_name: Name of the Kindred land to validate
|
||||
commander_tags: List of tags associated with the commander
|
||||
colors: List of colors in the deck
|
||||
|
||||
Returns:
|
||||
bool: True if the land meets criteria, False otherwise
|
||||
|
||||
Raises:
|
||||
KindredLandValidationError: If validation fails
|
||||
|
||||
Example:
|
||||
>>> validate_kindred_lands('Cavern of Souls', ['Elf Kindred'], ['G'])
|
||||
True
|
||||
"""
|
||||
try:
|
||||
# Check if any commander tags are Kindred-related
|
||||
has_kindred_theme = any('Kindred' in tag for tag in commander_tags)
|
||||
if not has_kindred_theme:
|
||||
return False
|
||||
|
||||
# Validate color requirements
|
||||
if land_name in KINDRED_STAPLE_LANDS:
|
||||
return True
|
||||
|
||||
# Additional validation logic can be added here
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
raise KindredLandValidationError(
|
||||
f"Failed to validate Kindred land {land_name}",
|
||||
{"error": str(e), "tags": commander_tags, "colors": colors}
|
||||
)
|
||||
|
||||
def get_available_kindred_lands(colors: List[str], commander_tags: List[str],
|
||||
price_checker: Optional[Any] = None,
|
||||
max_price: Optional[float] = None) -> List[str]:
|
||||
"""Get list of Kindred lands available for the deck's colors and themes.
|
||||
|
||||
Args:
|
||||
colors: List of deck colors
|
||||
commander_tags: List of commander theme tags
|
||||
price_checker: Optional price checker instance
|
||||
max_price: Maximum allowed price per card
|
||||
|
||||
Returns:
|
||||
List of available Kindred land names
|
||||
|
||||
Example:
|
||||
>>> get_available_kindred_lands(['G'], ['Elf Kindred'])
|
||||
['Cavern of Souls', 'Path of Ancestry', ...]
|
||||
"""
|
||||
# Start with staple Kindred lands
|
||||
available_lands = [land['name'] for land in KINDRED_STAPLE_LANDS]
|
||||
|
||||
# Filter by price if price checking is enabled
|
||||
if price_checker and max_price:
|
||||
available_lands = [
|
||||
land for land in available_lands
|
||||
if price_checker.get_card_price(land) <= max_price * 1.1
|
||||
]
|
||||
|
||||
return available_lands
|
||||
|
||||
def select_kindred_lands(available_lands: List[str], count: int,
|
||||
allow_duplicates: bool = False) -> List[str]:
|
||||
"""Select Kindred lands from the available pool.
|
||||
|
||||
Args:
|
||||
available_lands: List of available Kindred lands
|
||||
count: Number of Kindred lands to select
|
||||
allow_duplicates: Whether to allow duplicate selections
|
||||
|
||||
Returns:
|
||||
List of selected Kindred land names
|
||||
|
||||
Raises:
|
||||
KindredLandSelectionError: If unable to select required number of lands
|
||||
|
||||
Example:
|
||||
>>> select_kindred_lands(['Cavern of Souls', 'Path of Ancestry'], 2)
|
||||
['Path of Ancestry', 'Cavern of Souls']
|
||||
"""
|
||||
import random
|
||||
|
||||
if not available_lands:
|
||||
raise KindredLandSelectionError(
|
||||
"No Kindred lands available to select from",
|
||||
{"requested": count}
|
||||
)
|
||||
|
||||
if not allow_duplicates and count > len(available_lands):
|
||||
raise KindredLandSelectionError(
|
||||
f"Not enough unique Kindred lands available (requested {count}, have {len(available_lands)})",
|
||||
{"requested": count, "available": len(available_lands)}
|
||||
)
|
||||
|
||||
if allow_duplicates:
|
||||
return random.choices(available_lands, k=count)
|
||||
else:
|
||||
return random.sample(available_lands, k=count)
|
||||
|
||||
def process_kindred_lands(lands_to_add: List[str], card_library: pd.DataFrame,
|
||||
land_df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Update the land DataFrame by removing added Kindred lands.
|
||||
|
||||
Args:
|
||||
lands_to_add: List of Kindred land names to be added
|
||||
card_library: DataFrame containing all available cards
|
||||
land_df: DataFrame containing available lands
|
||||
|
||||
Returns:
|
||||
Updated land DataFrame with Kindred lands removed
|
||||
|
||||
Example:
|
||||
>>> process_kindred_lands(['Cavern of Souls'], card_library, land_df)
|
||||
DataFrame without 'Cavern of Souls' in the available lands
|
||||
"""
|
||||
updated_land_df = land_df[~land_df['name'].isin(lands_to_add)]
|
||||
return updated_land_df
|
1241
deck_builder.py
1241
deck_builder.py
File diff suppressed because it is too large
Load diff
468
exceptions.py
468
exceptions.py
|
@ -584,3 +584,471 @@ class CommanderThemeError(CommanderValidationError):
|
|||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code="CMD_THEME_ERR", details=details)
|
||||
|
||||
class CommanderMoveError(DeckBuilderError):
|
||||
"""Raised when there are issues moving the commander to the top of the library.
|
||||
|
||||
This exception is used when the commander_to_top() method encounters problems
|
||||
such as commander not found in library, invalid deck state, or other issues
|
||||
preventing the commander from being moved to the top position.
|
||||
|
||||
Examples:
|
||||
>>> raise CommanderMoveError(
|
||||
... "Commander not found in library",
|
||||
... {"commander_name": "Atraxa, Praetors' Voice"}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
"""Initialize commander move error.
|
||||
|
||||
Args:
|
||||
message: Description of the move operation failure
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code="CMD_MOVE_ERR", details=details)
|
||||
|
||||
class LibraryOrganizationError(DeckBuilderError):
|
||||
"""Base exception class for library organization errors.
|
||||
|
||||
This exception serves as the base for all errors related to organizing
|
||||
and managing the card library, including card type counting and validation.
|
||||
|
||||
Attributes:
|
||||
code (str): Error code for identifying the error type
|
||||
message (str): Descriptive error message
|
||||
details (dict): Additional error context and details
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, code: str = "LIB_ORG_ERR", details: dict | None = None):
|
||||
"""Initialize the library organization error.
|
||||
|
||||
Args:
|
||||
message: Human-readable error description
|
||||
code: Error code for identification and handling
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code=code, details=details)
|
||||
|
||||
class LibrarySortError(LibraryOrganizationError):
|
||||
"""Raised when there are issues sorting the card library.
|
||||
|
||||
This exception is used when the sort_library() method encounters problems
|
||||
organizing cards by type and name, such as invalid sort orders or
|
||||
card type categorization errors.
|
||||
|
||||
Examples:
|
||||
>>> raise LibrarySortError(
|
||||
... "Invalid card type sort order",
|
||||
... "Card type 'Unknown' not in sort order list"
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
"""Initialize library sort error.
|
||||
|
||||
Args:
|
||||
message: Description of the sorting failure
|
||||
details: Additional context about the error
|
||||
"""
|
||||
if details:
|
||||
details = details or {}
|
||||
details['sort_error'] = True
|
||||
super().__init__(message, code="LIB_SORT_ERR", details=details)
|
||||
|
||||
class DuplicateCardError(LibraryOrganizationError):
|
||||
"""Raised when there are issues processing duplicate cards in the library.
|
||||
|
||||
This exception is used when the concatenate_duplicates() method encounters problems
|
||||
processing duplicate cards, such as invalid card names, missing data, or
|
||||
inconsistencies in duplicate card information.
|
||||
|
||||
Examples:
|
||||
>>> raise DuplicateCardError(
|
||||
... "Failed to process duplicate cards",
|
||||
... "Sol Ring",
|
||||
... {"duplicate_count": 3}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, card_name: str | None = None, details: dict | None = None):
|
||||
"""Initialize duplicate card error.
|
||||
|
||||
Args:
|
||||
message: Description of the duplicate processing failure
|
||||
card_name: Name of the card causing the duplication error
|
||||
details: Additional context about the error
|
||||
"""
|
||||
if card_name:
|
||||
details = details or {}
|
||||
details['card_name'] = card_name
|
||||
super().__init__(message, code="DUPLICATE_CARD", details=details)
|
||||
|
||||
class CardTypeCountError(LibraryOrganizationError):
|
||||
"""Raised when there are issues counting cards of specific types.
|
||||
|
||||
This exception is used when card type counting operations fail or
|
||||
produce invalid results during library organization.
|
||||
|
||||
Examples:
|
||||
>>> raise CardTypeCountError(
|
||||
... "Invalid creature count",
|
||||
... "creature",
|
||||
... {"expected": 30, "actual": 15}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, card_type: str, details: dict | None = None):
|
||||
"""Initialize card type count error.
|
||||
|
||||
Args:
|
||||
message: Description of the counting failure
|
||||
card_type: The type of card that caused the counting error
|
||||
details: Additional context about the error
|
||||
"""
|
||||
if card_type:
|
||||
details = details or {}
|
||||
details['card_type'] = card_type
|
||||
super().__init__(message, code="CARD_TYPE_COUNT", details=details)
|
||||
|
||||
class ThemeError(DeckBuilderError):
|
||||
"""Base exception class for theme-related errors.
|
||||
|
||||
This exception serves as the base for all theme-related errors in the deck builder,
|
||||
including theme selection, validation, and weight calculation issues.
|
||||
|
||||
Attributes:
|
||||
code (str): Error code for identifying the error type
|
||||
message (str): Descriptive error message
|
||||
details (dict): Additional error context and details
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, code: str = "THEME_ERR", details: dict | None = None):
|
||||
"""Initialize the base theme error.
|
||||
|
||||
Args:
|
||||
message: Human-readable error description
|
||||
code: Error code for identification and handling
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code=code, details=details)
|
||||
|
||||
class ThemeSelectionError(ThemeError):
|
||||
"""Raised when theme selection fails or is invalid.
|
||||
|
||||
This exception is used when an invalid theme is selected or when
|
||||
the theme selection process is canceled by the user.
|
||||
|
||||
Examples:
|
||||
>>> raise ThemeSelectionError(
|
||||
... "Invalid theme selected",
|
||||
... "artifacts",
|
||||
... {"available_themes": ["tokens", "lifegain", "counters"]}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, selected_theme: str | None = None, details: dict | None = None):
|
||||
"""Initialize theme selection error.
|
||||
|
||||
Args:
|
||||
message: Description of the selection failure
|
||||
selected_theme: The invalid theme that was selected (if any)
|
||||
details: Additional context about the error
|
||||
"""
|
||||
if selected_theme:
|
||||
details = details or {}
|
||||
details['selected_theme'] = selected_theme
|
||||
super().__init__(message, code="THEME_SELECT", details=details)
|
||||
|
||||
class ThemeWeightError(ThemeError):
|
||||
"""Raised when theme weight calculation fails.
|
||||
|
||||
This exception is used when there are errors in calculating or validating
|
||||
theme weights during the theme selection process.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, theme: str | None = None, details: dict | None = None):
|
||||
"""Initialize theme weight error.
|
||||
|
||||
Args:
|
||||
message: Description of the weight calculation failure
|
||||
theme: The theme that caused the weight calculation error
|
||||
details: Additional context about the error
|
||||
"""
|
||||
if theme:
|
||||
details = details or {}
|
||||
details['theme'] = theme
|
||||
super().__init__(message, code="THEME_WEIGHT", details=details)
|
||||
|
||||
class IdealDeterminationError(DeckBuilderError):
|
||||
"""Raised when there are issues determining deck composition ideals.
|
||||
|
||||
This exception is used when the determine_ideals() method encounters problems
|
||||
calculating or validating deck composition ratios and requirements.
|
||||
|
||||
Examples:
|
||||
>>> raise IdealDeterminationError(
|
||||
... "Invalid land ratio calculation",
|
||||
... {"calculated_ratio": 0.1, "min_allowed": 0.3}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
"""Initialize ideal determination error.
|
||||
|
||||
Args:
|
||||
message: Description of the ideal calculation failure
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code="IDEAL_ERR", details=details)
|
||||
|
||||
class PriceConfigurationError(DeckBuilderError):
|
||||
"""Raised when there are issues configuring price settings.
|
||||
|
||||
This exception is used when price-related configuration in determine_ideals()
|
||||
is invalid or cannot be properly applied.
|
||||
|
||||
Examples:
|
||||
>>> raise PriceConfigurationError(
|
||||
... "Invalid budget allocation",
|
||||
... {"total_budget": 100, "min_card_price": 200}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
"""Initialize price configuration error.
|
||||
|
||||
Args:
|
||||
message: Description of the price configuration failure
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code="PRICE_CONFIG_ERR", details=details)
|
||||
|
||||
class BasicLandError(DeckBuilderError):
|
||||
"""Base exception class for basic land related errors.
|
||||
|
||||
This exception serves as the base for all basic land related errors in the deck builder,
|
||||
including land distribution, snow-covered lands, and colorless deck handling.
|
||||
|
||||
Attributes:
|
||||
code (str): Error code for identifying the error type
|
||||
message (str): Descriptive error message
|
||||
details (dict): Additional error context and details
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, code: str = "BASIC_LAND_ERR", details: dict | None = None):
|
||||
"""Initialize the basic land error.
|
||||
|
||||
Args:
|
||||
message: Human-readable error description
|
||||
code: Error code for identification and handling
|
||||
"""
|
||||
super().__init__(message, code=code, details=details)
|
||||
|
||||
class BasicLandCountError(BasicLandError):
|
||||
"""Raised when there are issues with counting basic lands.
|
||||
|
||||
This exception is used when basic land counting operations fail or
|
||||
produce unexpected results during deck validation or analysis.
|
||||
|
||||
Examples:
|
||||
>>> raise BasicLandCountError(
|
||||
... "Failed to count basic lands in deck",
|
||||
... {"expected_count": 35, "actual_count": 0}
|
||||
... )
|
||||
|
||||
>>> raise BasicLandCountError(
|
||||
... "Invalid basic land count for color distribution",
|
||||
... {"color": "U", "count": -1}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
"""Initialize basic land count error.
|
||||
|
||||
Args:
|
||||
message: Description of the counting operation failure
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code="BASIC_LAND_COUNT_ERR", details=details)
|
||||
|
||||
class StapleLandError(DeckBuilderError):
|
||||
"""Raised when there are issues adding staple lands.
|
||||
```
|
||||
This exception is used when there are problems adding staple lands
|
||||
to the deck, such as invalid land types, missing lands, or
|
||||
incompatible color requirements.
|
||||
|
||||
Examples:
|
||||
>>> raise StapleLandError(
|
||||
... "Failed to add required shock lands",
|
||||
... {"missing_lands": ["Steam Vents", "Breeding Pool"]}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
"""Initialize staple land error.
|
||||
|
||||
Args:
|
||||
message: Description of the staple land operation failure
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(
|
||||
message,
|
||||
code="STAPLE_LAND_ERR",
|
||||
details=details
|
||||
)
|
||||
|
||||
class LandDistributionError(BasicLandError):
|
||||
"""Raised when there are issues with basic land distribution.
|
||||
|
||||
This exception is used when there are problems distributing basic lands
|
||||
across colors, such as invalid color ratios or unsupported color combinations.
|
||||
|
||||
Examples:
|
||||
>>> raise LandDistributionError(
|
||||
... "Invalid land distribution for colorless deck",
|
||||
... {"colors": [], "requested_lands": 40}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
"""Initialize land distribution error.
|
||||
|
||||
Args:
|
||||
message: Description of the land distribution failure
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code="LAND_DIST_ERR", details=details)
|
||||
|
||||
class FetchLandError(DeckBuilderError):
|
||||
"""Base exception class for fetch land-related errors.
|
||||
|
||||
This exception serves as the base for all fetch land-related errors in the deck builder,
|
||||
including validation errors, selection errors, and fetch land processing issues.
|
||||
|
||||
Attributes:
|
||||
code (str): Error code for identifying the error type
|
||||
message (str): Descriptive error message
|
||||
details (dict): Additional error context and details
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, code: str = "FETCH_ERR", details: dict | None = None):
|
||||
"""Initialize the base fetch land error.
|
||||
|
||||
Args:
|
||||
message: Human-readable error description
|
||||
code: Error code for identification and handling
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code=code, details=details)
|
||||
|
||||
class KindredLandError(DeckBuilderError):
|
||||
"""Base exception class for Kindred land-related errors.
|
||||
|
||||
This exception serves as the base for all Kindred land-related errors in the deck builder,
|
||||
including validation errors, selection errors, and Kindred land processing issues.
|
||||
|
||||
Attributes:
|
||||
code (str): Error code for identifying the error type
|
||||
message (str): Descriptive error message
|
||||
details (dict): Additional error context and details
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, code: str = "KINDRED_ERR", details: dict | None = None):
|
||||
"""Initialize the base Kindred land error.
|
||||
|
||||
Args:
|
||||
message: Human-readable error description
|
||||
code: Error code for identification and handling
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code=code, details=details)
|
||||
|
||||
class KindredLandValidationError(KindredLandError):
|
||||
"""Raised when Kindred land validation fails.
|
||||
|
||||
This exception is used when there are issues validating Kindred land inputs,
|
||||
such as invalid land types, unsupported creature types, or color identity mismatches.
|
||||
|
||||
Examples:
|
||||
>>> raise KindredLandValidationError(
|
||||
... "Invalid Kindred land type",
|
||||
... {"land_type": "Non-Kindred Land", "creature_type": "Elf"}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
"""Initialize Kindred land validation error.
|
||||
|
||||
Args:
|
||||
message: Description of the validation failure
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code="KINDRED_VALID_ERR", details=details)
|
||||
|
||||
class KindredLandSelectionError(KindredLandError):
|
||||
"""Raised when Kindred land selection fails.
|
||||
|
||||
This exception is used when there are issues selecting appropriate Kindred lands,
|
||||
such as no valid lands found, creature type mismatches, or price constraints.
|
||||
|
||||
Examples:
|
||||
>>> raise KindredLandSelectionError(
|
||||
... "No valid Kindred lands found for creature type",
|
||||
... {"creature_type": "Dragon", "attempted_lands": ["Cavern of Souls"]}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
"""Initialize Kindred land selection error.
|
||||
|
||||
Args:
|
||||
message: Description of the selection failure
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code="KINDRED_SELECT_ERR", details=details)
|
||||
|
||||
class FetchLandValidationError(FetchLandError):
|
||||
"""Raised when fetch land validation fails.
|
||||
|
||||
This exception is used when there are issues validating fetch land inputs,
|
||||
such as invalid fetch count, unsupported colors, or invalid fetch land types.
|
||||
|
||||
Examples:
|
||||
>>> raise FetchLandValidationError(
|
||||
... "Invalid fetch land count",
|
||||
... {"requested": 10, "maximum": 9}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
"""Initialize fetch land validation error.
|
||||
|
||||
Args:
|
||||
message: Description of the validation failure
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code="FETCH_VALID_ERR", details=details)
|
||||
|
||||
class FetchLandSelectionError(FetchLandError):
|
||||
"""Raised when fetch land selection fails.
|
||||
|
||||
This exception is used when there are issues selecting appropriate fetch lands,
|
||||
such as no valid fetches found, color identity mismatches, or price constraints.
|
||||
|
||||
Examples:
|
||||
>>> raise FetchLandSelectionError(
|
||||
... "No valid fetch lands found for color identity",
|
||||
... {"colors": ["W", "U"], "attempted_fetches": ["Flooded Strand", "Polluted Delta"]}
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
"""Initialize fetch land selection error.
|
||||
|
||||
Args:
|
||||
message: Description of the selection failure
|
||||
details: Additional context about the error
|
||||
"""
|
||||
super().__init__(message, code="FETCH_SELECT_ERR", details=details)
|
|
@ -190,19 +190,19 @@ class InputHandler:
|
|||
question = [
|
||||
inquirer.Text(
|
||||
'text',
|
||||
message=message or 'Enter text',
|
||||
message=f'{message}' or 'Enter text',
|
||||
default=default_value or self.default_text
|
||||
)
|
||||
]
|
||||
result = inquirer.prompt(question)['text']
|
||||
if self.validate_text(result):
|
||||
return result
|
||||
return str(result)
|
||||
|
||||
elif question_type == 'Price':
|
||||
question = [
|
||||
inquirer.Text(
|
||||
'price',
|
||||
message=message or 'Enter price (or "unlimited")',
|
||||
message=f'{message}' or 'Enter price (or "unlimited")',
|
||||
default=str(default_value or DEFAULT_MAX_CARD_PRICE)
|
||||
)
|
||||
]
|
||||
|
@ -210,12 +210,13 @@ class InputHandler:
|
|||
price, is_unlimited = self.validate_price(result)
|
||||
if not is_unlimited:
|
||||
self.validate_price_threshold(price)
|
||||
return price
|
||||
return float(price)
|
||||
|
||||
elif question_type == 'Number':
|
||||
question = [
|
||||
inquirer.Text(
|
||||
'number',
|
||||
message=message or 'Enter number',
|
||||
message=f'{message}' or 'Enter number',
|
||||
default=str(default_value or self.default_number)
|
||||
)
|
||||
]
|
||||
|
@ -226,7 +227,7 @@ class InputHandler:
|
|||
question = [
|
||||
inquirer.Confirm(
|
||||
'confirm',
|
||||
message=message or 'Confirm?',
|
||||
message=f'{message}' or 'Confirm?',
|
||||
default=default_value if default_value is not None else self.default_confirm
|
||||
)
|
||||
]
|
||||
|
@ -239,7 +240,7 @@ class InputHandler:
|
|||
question = [
|
||||
inquirer.List(
|
||||
'selection',
|
||||
message=message or 'Select an option',
|
||||
message=f'{message}' or 'Select an option',
|
||||
choices=choices_list,
|
||||
carousel=True
|
||||
)
|
||||
|
|
193
settings.py
193
settings.py
|
@ -1,12 +1,14 @@
|
|||
from typing import Dict, List, Optional, Final, Tuple, Pattern, Union
|
||||
from typing import Dict, List, Optional, Final, Tuple, Pattern, Union, Callable
|
||||
import ast
|
||||
|
||||
# Commander selection configuration
|
||||
# Format string for displaying duplicate cards in deck lists
|
||||
DUPLICATE_CARD_FORMAT: Final[str] = '{card_name} x {count}'
|
||||
|
||||
COMMANDER_CSV_PATH: Final[str] = 'csv_files/commander_cards.csv'
|
||||
FUZZY_MATCH_THRESHOLD: Final[int] = 90 # Threshold for fuzzy name matching
|
||||
MAX_FUZZY_CHOICES: Final[int] = 5 # Maximum number of fuzzy match choices
|
||||
COMMANDER_CONVERTERS: Final[Dict[str, str]] = {'themeTags': ast.literal_eval, 'creatureTypes': ast.literal_eval} # CSV loading converters
|
||||
|
||||
# Commander-related constants
|
||||
COMMANDER_POWER_DEFAULT: Final[int] = 0
|
||||
COMMANDER_TOUGHNESS_DEFAULT: Final[int] = 0
|
||||
|
@ -27,6 +29,148 @@ PRICE_CACHE_SIZE: Final[int] = 128 # Size of price check LRU cache
|
|||
PRICE_CHECK_TIMEOUT: Final[int] = 30 # Timeout for price check requests in seconds
|
||||
PRICE_TOLERANCE_MULTIPLIER: Final[float] = 1.1 # Multiplier for price tolerance
|
||||
DEFAULT_MAX_CARD_PRICE: Final[float] = 20.0 # Default maximum price per card
|
||||
|
||||
# Deck composition defaults
|
||||
DEFAULT_RAMP_COUNT: Final[int] = 8 # Default number of ramp pieces
|
||||
DEFAULT_LAND_COUNT: Final[int] = 35 # Default total land count
|
||||
DEFAULT_BASIC_LAND_COUNT: Final[int] = 20 # Default minimum basic lands
|
||||
DEFAULT_NON_BASIC_LAND_SLOTS: Final[int] = 10 # Default number of non-basic land slots to reserve
|
||||
DEFAULT_BASICS_PER_COLOR: Final[int] = 5 # Default number of basic lands to add per color
|
||||
|
||||
# Default fetch land count
|
||||
FETCH_LAND_DEFAULT_COUNT: Final[int] = 3 # Default number of fetch lands to include
|
||||
# Basic land mappings
|
||||
COLOR_TO_BASIC_LAND: Final[Dict[str, str]] = {
|
||||
'W': 'Plains',
|
||||
'U': 'Island',
|
||||
'B': 'Swamp',
|
||||
'R': 'Mountain',
|
||||
'G': 'Forest',
|
||||
'C': 'Wastes'
|
||||
}
|
||||
|
||||
SNOW_COVERED_BASIC_LANDS: Final[Dict[str, str]] = {
|
||||
'W': 'Snow-Covered Plains',
|
||||
'U': 'Snow-Covered Island',
|
||||
'B': 'Snow-Covered Swamp',
|
||||
'G': 'Snow-Covered Forest'
|
||||
}
|
||||
|
||||
SNOW_BASIC_LAND_MAPPING: Final[Dict[str, str]] = {
|
||||
'W': 'Snow-Covered Plains',
|
||||
'U': 'Snow-Covered Island',
|
||||
'B': 'Snow-Covered Swamp',
|
||||
'R': 'Snow-Covered Mountain',
|
||||
'G': 'Snow-Covered Forest',
|
||||
'C': 'Wastes' # Note: No snow-covered version exists for Wastes
|
||||
}
|
||||
|
||||
# Generic fetch lands list
|
||||
GENERIC_FETCH_LANDS: Final[List[str]] = [
|
||||
'Evolving Wilds',
|
||||
'Terramorphic Expanse',
|
||||
'Shire Terrace',
|
||||
'Escape Tunnel',
|
||||
'Promising Vein',
|
||||
'Myriad Landscape',
|
||||
'Fabled Passage',
|
||||
'Terminal Moraine',
|
||||
'Prismatic Vista'
|
||||
]
|
||||
|
||||
# Kindred land constants
|
||||
KINDRED_STAPLE_LANDS: Final[List[Dict[str, str]]] = [
|
||||
{
|
||||
'name': 'Path of Ancestry',
|
||||
'type': 'Land'
|
||||
},
|
||||
{
|
||||
'name': 'Three Tree City',
|
||||
'type': 'Legendary Land'
|
||||
},
|
||||
{'name': 'Cavern of Souls', 'type': 'Land'}
|
||||
]
|
||||
|
||||
# Color-specific fetch land mappings
|
||||
COLOR_TO_FETCH_LANDS: Final[Dict[str, List[str]]] = {
|
||||
'W': [
|
||||
'Flooded Strand',
|
||||
'Windswept Heath',
|
||||
'Marsh Flats',
|
||||
'Arid Mesa',
|
||||
'Brokers Hideout',
|
||||
'Obscura Storefront',
|
||||
'Cabaretti Courtyard'
|
||||
],
|
||||
'U': [
|
||||
'Flooded Strand',
|
||||
'Polluted Delta',
|
||||
'Scalding Tarn',
|
||||
'Misty Rainforest',
|
||||
'Brokers Hideout',
|
||||
'Obscura Storefront',
|
||||
'Maestros Theater'
|
||||
],
|
||||
'B': [
|
||||
'Polluted Delta',
|
||||
'Bloodstained Mire',
|
||||
'Marsh Flats',
|
||||
'Verdant Catacombs',
|
||||
'Obscura Storefront',
|
||||
'Maestros Theater',
|
||||
'Riveteers Overlook'
|
||||
],
|
||||
'R': [
|
||||
'Bloodstained Mire',
|
||||
'Wooded Foothills',
|
||||
'Scalding Tarn',
|
||||
'Arid Mesa',
|
||||
'Maestros Theater',
|
||||
'Riveteers Overlook',
|
||||
'Cabaretti Courtyard'
|
||||
],
|
||||
'G': [
|
||||
'Wooded Foothills',
|
||||
'Windswept Heath',
|
||||
'Verdant Catacombs',
|
||||
'Misty Rainforest',
|
||||
'Brokers Hideout',
|
||||
'Riveteers Overlook',
|
||||
'Cabaretti Courtyard'
|
||||
]
|
||||
}
|
||||
|
||||
DEFAULT_CREATURE_COUNT: Final[int] = 25 # Default number of creatures
|
||||
DEFAULT_REMOVAL_COUNT: Final[int] = 10 # Default number of spot removal spells
|
||||
DEFAULT_WIPES_COUNT: Final[int] = 2 # Default number of board wipes
|
||||
|
||||
# Staple land conditions mapping
|
||||
STAPLE_LAND_CONDITIONS: Final[Dict[str, Callable[[List[str], List[str], int], bool]]] = {
|
||||
'Reliquary Tower': lambda commander_tags, colors, commander_power: True, # Always include
|
||||
'Ash Barrens': lambda commander_tags, colors, commander_power: 'Landfall' not in commander_tags,
|
||||
'Command Tower': lambda commander_tags, colors, commander_power: len(colors) > 1,
|
||||
'Exotic Orchard': lambda commander_tags, colors, commander_power: len(colors) > 1,
|
||||
'War Room': lambda commander_tags, colors, commander_power: len(colors) <= 2,
|
||||
'Rogue\'s Passage': lambda commander_tags, colors, commander_power: commander_power >= 5
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_CARD_ADVANTAGE_COUNT: Final[int] = 10 # Default number of card advantage pieces
|
||||
DEFAULT_PROTECTION_COUNT: Final[int] = 8 # Default number of protection spells
|
||||
|
||||
# Deck composition prompts
|
||||
DECK_COMPOSITION_PROMPTS: Final[Dict[str, str]] = {
|
||||
'ramp': 'Enter desired number of ramp pieces (default: 8):',
|
||||
'lands': 'Enter desired number of total lands (default: 35):',
|
||||
'basic_lands': 'Enter minimum number of basic lands (default: 20):',
|
||||
'creatures': 'Enter desired number of creatures (default: 25):',
|
||||
'removal': 'Enter desired number of spot removal spells (default: 10):',
|
||||
'wipes': 'Enter desired number of board wipes (default: 2):',
|
||||
'card_advantage': 'Enter desired number of card advantage pieces (default: 10):',
|
||||
'protection': 'Enter desired number of protection spells (default: 8):',
|
||||
'max_deck_price': 'Enter maximum total deck price in dollars (default: 400.0):',
|
||||
'max_card_price': 'Enter maximum price per card in dollars (default: 20.0):'
|
||||
}
|
||||
DEFAULT_MAX_DECK_PRICE: Final[float] = 400.0 # Default maximum total deck price
|
||||
BATCH_PRICE_CHECK_SIZE: Final[int] = 50 # Number of cards to check prices for in one batch
|
||||
# Constants for input validation
|
||||
|
@ -555,11 +699,31 @@ BOARD_WIPE_EXCLUSION_PATTERNS = [
|
|||
'target player\'s library',
|
||||
'that player\'s library'
|
||||
]
|
||||
|
||||
|
||||
CARD_TYPES = ['Artifact','Creature', 'Enchantment', 'Instant', 'Land', 'Planeswalker', 'Sorcery',
|
||||
'Kindred', 'Dungeon', 'Battle']
|
||||
|
||||
# Card type sorting order for organizing libraries
|
||||
# This constant defines the order in which different card types should be sorted
|
||||
# when organizing a deck library. The order is designed to group cards logically,
|
||||
# starting with Planeswalkers and ending with Lands.
|
||||
CARD_TYPE_SORT_ORDER: Final[List[str]] = [
|
||||
'Planeswalker', 'Battle', 'Creature', 'Instant', 'Sorcery',
|
||||
'Artifact', 'Enchantment', 'Land'
|
||||
]
|
||||
|
||||
# Default counts for each card type
|
||||
CARD_TYPE_COUNT_DEFAULTS: Final[Dict[str, int]] = {
|
||||
'Artifact': 0,
|
||||
'Battle': 0,
|
||||
'Creature': 0,
|
||||
'Enchantment': 0,
|
||||
'Instant': 0,
|
||||
'Kindred': 0,
|
||||
'Land': 0,
|
||||
'Planeswalker': 0,
|
||||
'Sorcery': 0
|
||||
}
|
||||
|
||||
# Mapping of card types to their corresponding theme tags
|
||||
TYPE_TAG_MAPPING = {
|
||||
'Artifact': ['Artifacts Matter'],
|
||||
|
@ -810,6 +974,21 @@ REQUIRED_COLUMNS: List[str] = [
|
|||
'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side'
|
||||
]
|
||||
|
||||
# Constants for theme weight management
|
||||
THEME_WEIGHTS_DEFAULT: Final[Dict[str, float]] = {
|
||||
'primary': 1.0,
|
||||
'secondary': 0.6,
|
||||
'tertiary': 0.3,
|
||||
'hidden': 0.0
|
||||
}
|
||||
|
||||
WEIGHT_ADJUSTMENT_FACTORS: Final[Dict[str, float]] = {
|
||||
'kindred_primary': 1.5, # Boost for Kindred themes as primary
|
||||
'kindred_secondary': 1.3, # Boost for Kindred themes as secondary
|
||||
'kindred_tertiary': 1.2, # Boost for Kindred themes as tertiary
|
||||
'theme_synergy': 1.2 # Boost for themes that work well together
|
||||
}
|
||||
|
||||
DEFAULT_THEME_TAGS = [
|
||||
'Aggro', 'Aristocrats', 'Artifacts Matter', 'Big Mana', 'Blink',
|
||||
'Board Wipes', 'Burn', 'Cantrips', 'Card Draw', 'Clones',
|
||||
|
@ -976,9 +1155,9 @@ DATAFRAME_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float, boo
|
|||
|
||||
# Card category validation rules
|
||||
CREATURE_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float, bool]]]] = {
|
||||
'power': {'type': ('str', 'int', 'float'), 'required': True},
|
||||
'toughness': {'type': ('str', 'int', 'float'), 'required': True},
|
||||
'creatureTypes': {'type': 'list', 'required': True}
|
||||
'power': {'type': ('str', 'int', 'float', 'object'), 'required': True},
|
||||
'toughness': {'type': ('str', 'int', 'float', 'object'), 'required': True},
|
||||
'creatureTypes': {'type': ('list', 'object'), 'required': True}
|
||||
}
|
||||
|
||||
SPELL_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float, bool]]]] = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue