2024-12-05 16:54:56 -08:00
from __future__ import annotations
2024-12-26 10:52:09 -08:00
import logging
2024-12-05 16:54:56 -08:00
import inquirer . prompt # type: ignore
2024-12-20 07:10:53 -08:00
import keyboard # type: ignore
2024-12-26 11:59:30 -08:00
import math
2024-12-25 19:56:35 -08:00
import numpy as np
2024-12-05 16:54:56 -08:00
import pandas as pd # type: ignore
import pprint # type: ignore
2024-12-06 12:04:39 -08:00
import random
2024-12-21 00:58:12 -08:00
import time
2024-12-05 16:54:56 -08:00
2024-12-25 19:56:35 -08:00
from functools import lru_cache
2024-12-26 10:52:09 -08:00
from fuzzywuzzy import process # type: ignore
2024-12-05 16:54:56 -08:00
2024-12-25 19:56:35 -08:00
from settings import basic_lands , card_types , csv_directory , multiple_copy_cards
2024-12-26 11:59:30 -08:00
from setup import determine_commanders
2024-12-06 12:04:39 -08:00
2024-12-20 22:02:35 -08:00
try :
import scrython # type: ignore
2024-12-20 13:39:23 -08:00
use_scrython = True
2024-12-20 22:02:35 -08:00
except ImportError :
scrython = None
2024-12-20 13:39:23 -08:00
use_scrython = False
2024-12-26 10:52:09 -08:00
logging . warning ( " Scrython is not installed. Some pricing features will be unavailable. " )
logging . basicConfig (
level = logging . INFO ,
format = ' %(asctime)s - %(levelname)s - %(message)s '
)
2024-12-20 22:02:35 -08:00
2024-12-06 12:04:39 -08:00
pd . set_option ( ' display.max_columns ' , None )
pd . set_option ( ' display.max_rows ' , None )
2024-12-25 19:56:35 -08:00
pd . set_option ( ' display.max_colwidth ' , 50 )
2024-12-06 12:04:39 -08:00
2024-12-27 12:02:50 -08:00
"""
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 .
2024-12-05 16:54:56 -08:00
2024-12-27 12:02:50 -08:00
Currently , the script will ask questions to determine number of
creatures , lands , interaction , ramp , etc . . . then add cards and
adjust from there .
2024-12-05 16:54:56 -08:00
2024-12-27 12:02:50 -08:00
Land spread will ideally be handled based on pips and some adjustment
is planned based on mana curve and ramp added .
"""
2024-12-05 16:54:56 -08:00
2024-12-30 11:43:36 -08:00
def new_line ( num_lines : int = 1 ) - > None :
""" Print specified number of newlines for formatting output.
Args :
num_lines ( int ) : Number of newlines to print . Defaults to 1.
Returns :
None
"""
if num_lines < 0 :
raise ValueError ( " Number of lines cannot be negative " )
print ( ' \n ' * num_lines )
2024-12-25 19:56:35 -08:00
2024-12-05 16:54:56 -08:00
class DeckBuilder :
2024-12-30 11:43:36 -08:00
2024-12-05 16:54:56 -08:00
def __init__ ( self ) :
2024-12-25 19:56:35 -08:00
self . card_library = pd . DataFrame ( )
self . card_library [ ' Card Name ' ] = pd . Series ( dtype = ' str ' )
self . card_library [ ' Card Type ' ] = pd . Series ( dtype = ' str ' )
self . card_library [ ' Mana Cost ' ] = pd . Series ( dtype = ' str ' )
self . card_library [ ' Mana Value ' ] = pd . Series ( dtype = ' int ' )
2024-12-29 20:16:57 -08:00
self . card_library [ ' Commander ' ] = pd . Series ( dtype = ' bool ' )
2024-12-25 19:56:35 -08:00
self . set_max_deck_price = False
self . set_max_card_price = False
2024-12-26 10:52:09 -08:00
self . card_prices = { } if use_scrython else None
2024-12-25 19:56:35 -08:00
2024-12-30 11:43:36 -08:00
def pause_with_message ( self , message = " Press Enter to continue... " ) :
""" Helper function to pause execution with a message. """
print ( f " \n { message } " )
input ( )
def validate_text ( self , result : str ) - > bool :
""" Validate text input is not empty.
Args :
result ( str ) : Text input to validate
Returns :
bool : True if text is not empty after stripping whitespace
"""
2024-12-26 10:52:09 -08:00
return bool ( result and result . strip ( ) )
2024-12-30 11:43:36 -08:00
def validate_number ( self , result : str ) - > float | None :
""" Validate and convert string input to float.
Args :
result ( str ) : Number input to validate
Returns :
float | None : Converted float value or None if invalid
"""
2024-12-26 10:52:09 -08:00
try :
return float ( result )
2024-12-30 11:43:36 -08:00
except ( ValueError , TypeError ) :
2024-12-26 10:52:09 -08:00
return None
def validate_confirm ( self , result ) :
return bool ( result )
def questionnaire ( self , question_type , default_value = ' ' , choices_list = [ ] ) :
MAX_ATTEMPTS = 3
if question_type == ' Text ' :
question = [ inquirer . Text ( ' text ' ) ]
2024-12-20 22:02:35 -08:00
result = inquirer . prompt ( question ) [ ' text ' ]
2024-12-26 10:52:09 -08:00
while not result . strip ( ) :
2024-12-20 22:02:35 -08:00
question = [
2024-12-26 10:52:09 -08:00
inquirer . Text ( ' text ' , message = ' Input cannot be empty ' )
2024-12-20 22:02:35 -08:00
]
result = inquirer . prompt ( question ) [ ' text ' ]
return result
2024-12-26 10:52:09 -08:00
elif question_type == ' Number ' :
attempts = 0
2024-12-20 22:02:35 -08:00
question = [
2024-12-26 10:52:09 -08:00
inquirer . Text ( ' number ' , default = default_value )
]
result = inquirer . prompt ( question ) [ ' number ' ]
while attempts < MAX_ATTEMPTS :
try :
result = float ( result )
break
except ValueError :
attempts + = 1
if attempts < MAX_ATTEMPTS :
question = [
inquirer . Text ( ' number ' ,
message = ' Input must be a valid number ' ,
default = default_value )
]
result = inquirer . prompt ( question ) [ ' number ' ]
else :
logging . error ( " Maximum input attempts reached for Number type. " )
raise ValueError ( " Invalid number input. " )
2024-12-20 22:02:35 -08:00
return result
2024-12-26 10:52:09 -08:00
elif question_type == ' Confirm ' :
2024-12-20 22:02:35 -08:00
question = [
2024-12-26 10:52:09 -08:00
inquirer . Confirm ( ' confirm ' , default = default_value )
2024-12-20 22:02:35 -08:00
]
result = inquirer . prompt ( question ) [ ' confirm ' ]
2024-12-26 10:52:09 -08:00
return self . validate_confirm ( result )
elif question_type == ' Choice ' :
2024-12-20 22:02:35 -08:00
question = [
inquirer . List ( ' selection ' ,
2024-12-26 10:52:09 -08:00
choices = choices_list ,
carousel = True )
2024-12-20 22:02:35 -08:00
]
result = inquirer . prompt ( question ) [ ' selection ' ]
return result
2024-12-26 10:52:09 -08:00
raise ValueError ( f " Unsupported question type: { question_type } " )
2024-12-20 22:02:35 -08:00
2024-12-25 19:56:35 -08:00
@lru_cache ( maxsize = 128 )
2024-12-20 13:39:23 -08:00
def price_check ( self , card_name ) :
2024-12-25 19:56:35 -08:00
try :
time . sleep ( 0.1 )
card = scrython . cards . Named ( fuzzy = card_name )
card_price = card . prices ( ' usd ' )
2024-12-27 12:02:50 -08:00
if card_price is not None and isinstance ( card_price , ( int , float ) ) :
2024-12-25 19:56:35 -08:00
try :
2024-12-26 10:52:09 -08:00
self . card_prices [ card_name ] = card_price
2024-12-25 19:56:35 -08:00
return float ( card_price )
except ValueError :
2024-12-27 12:02:50 -08:00
logging . error ( f " Invalid price format for ' { card_name } ' : { card_price } " )
return 0.0
return 0.0
2024-12-30 11:43:36 -08:00
except ( scrython . foundation . ScryfallError , scrython . foundation . ScryfallRequestError ) as e :
2024-12-27 12:02:50 -08:00
logging . error ( f " Scryfall API error for ' { card_name } ' : { e } " )
return 0.0
except TimeoutError :
logging . error ( f " Request timed out while fetching price for ' { card_name } ' " )
2024-12-25 19:56:35 -08:00
return 0.0
except Exception as e :
2024-12-27 12:02:50 -08:00
logging . error ( f " Unexpected error fetching price for ' { card_name } ' : { e } " )
2024-12-25 19:56:35 -08:00
return 0.0
2024-12-27 12:02:50 -08:00
2024-12-05 16:54:56 -08:00
def determine_commander ( self ) :
2024-12-07 23:34:14 -08:00
# Setup dataframe
try :
2024-12-19 12:02:26 -08:00
df = pd . read_csv ( ' csv_files/commander_cards.csv ' , converters = { ' themeTags ' : pd . eval , ' creatureTypes ' : pd . eval } )
2024-12-07 23:34:14 -08:00
except FileNotFoundError :
2024-12-19 12:02:26 -08:00
determine_commanders ( )
df = pd . read_csv ( ' csv_files/commander_cards.csv ' , converters = { ' themeTags ' : pd . eval , ' creatureTypes ' : pd . eval } )
2024-12-05 16:54:56 -08:00
# Determine the commander of the deck
2024-12-27 12:02:50 -08:00
# Set frames that have nothing for color identity to be 'COLORLESS' instead
df [ ' colorIdentity ' ] = df [ ' colorIdentity ' ] . fillna ( ' COLORLESS ' )
df [ ' colors ' ] = df [ ' colors ' ] . fillna ( ' COLORLESS ' )
2024-12-05 16:54:56 -08:00
commander_chosen = False
while not commander_chosen :
print ( ' Enter a card name to be your commander, note that at this time only cards that have the \' Creature \' type may be chosen ' )
2024-12-20 22:02:35 -08:00
card_choice = self . questionnaire ( ' Text ' , ' ' )
2024-12-30 11:43:36 -08:00
2024-12-19 12:02:26 -08:00
# Logic to find the card in the commander_cards csv, then display it's information
2024-12-07 23:34:14 -08:00
# If the card can't be found, or doesn't have enough of a match score, display a
# list to choose from
fuzzy_chosen = False
while not fuzzy_chosen :
2024-12-31 09:18:02 -08:00
match , score , _ = process . extractOne ( card_choice , df [ ' name ' ] )
2024-12-07 23:34:14 -08:00
if score > = 90 :
fuzzy_card_choice = match
print ( fuzzy_card_choice )
fuzzy_chosen = True
else :
2024-12-31 09:18:02 -08:00
logging . warning ( ' Multiple options found, which is correct? ' )
2024-12-07 23:34:14 -08:00
fuzzy_card_choices = process . extract ( card_choice , df [ ' name ' ] , limit = 5 )
fuzzy_card_choices . append ( ' Neither ' )
print ( fuzzy_card_choices )
2024-12-26 11:59:30 -08:00
fuzzy_card_choice = self . questionnaire ( ' Choice ' , choices_list = fuzzy_card_choices )
2024-12-30 11:43:36 -08:00
if isinstance ( fuzzy_card_choice , tuple ) :
2024-12-07 23:34:14 -08:00
fuzzy_card_choice = fuzzy_card_choice [ 0 ]
2024-12-30 11:43:36 -08:00
if fuzzy_card_choice != ' Neither ' :
2024-12-07 23:34:14 -08:00
print ( fuzzy_card_choice )
fuzzy_chosen = True
2024-12-30 11:43:36 -08:00
2024-12-07 23:34:14 -08:00
else :
break
2024-12-30 11:43:36 -08:00
2024-12-07 23:34:14 -08:00
filtered_df = df [ df [ ' name ' ] == fuzzy_card_choice ]
df_dict = filtered_df . to_dict ( ' list ' )
print ( ' Is this the card you chose? ' )
pprint . pprint ( df_dict , sort_dicts = False )
self . commander_df = pd . DataFrame ( df_dict )
2024-12-30 11:43:36 -08:00
2024-12-07 23:34:14 -08:00
# Confirm if card entered was correct
2024-12-20 22:02:35 -08:00
commander_confirmed = self . questionnaire ( ' Confirm ' , True )
2024-12-07 23:34:14 -08:00
# If correct, set it as the commander
if commander_confirmed :
commander_chosen = True
self . commander_info = df_dict
self . commander = self . commander_df . at [ 0 , ' name ' ]
2024-12-20 22:02:35 -08:00
self . price_check ( self . commander )
2024-12-30 11:43:36 -08:00
logging . info ( f " Commander selected: { self . commander } " )
2024-12-07 23:34:14 -08:00
break
else :
commander_chosen = False
2024-12-06 12:04:39 -08:00
# Send commander info to setup commander, including extracting info on colors, color identity,
# creature types, and other information, like keywords, abilities, etc...
self . commander_setup ( )
def commander_setup ( self ) :
# Load commander info into a dataframe
df = self . commander_df
2024-12-30 11:43:36 -08:00
2024-12-19 12:02:26 -08:00
# Set type line
self . commander_type = str ( df . at [ 0 , ' type ' ] )
2024-12-30 11:43:36 -08:00
2024-12-19 12:02:26 -08:00
# Set text line
self . commander_text = str ( df . at [ 0 , ' text ' ] )
2024-12-30 11:43:36 -08:00
2024-12-19 12:02:26 -08:00
# Set Power
2024-12-06 12:04:39 -08:00
self . commander_power = int ( df . at [ 0 , ' power ' ] )
2024-12-30 11:43:36 -08:00
2024-12-19 12:02:26 -08:00
# Set Toughness
self . commander_toughness = int ( df . at [ 0 , ' toughness ' ] )
2024-12-30 11:43:36 -08:00
2024-12-19 12:02:26 -08:00
# Set Mana Cost
self . commander_mana_cost = str ( df . at [ 0 , ' manaCost ' ] )
2024-12-29 20:16:57 -08:00
self . commander_mana_value = int ( df . at [ 0 , ' manaValue ' ] )
2024-12-30 11:43:36 -08:00
2024-12-06 12:04:39 -08:00
# Set color identity
2024-12-30 11:43:36 -08:00
try :
self . color_identity = df . at [ 0 , ' colorIdentity ' ]
if pd . isna ( self . color_identity ) :
self . color_identity = ' COLORLESS '
self . color_identity_full = ' '
self . determine_color_identity ( )
except Exception as e :
logging . error ( f " Failed to set color identity: { e } " )
raise ValueError ( " Could not determine color identity " ) from e
2024-12-06 12:04:39 -08:00
# Set creature colors
2024-12-27 12:02:50 -08:00
if pd . notna ( df . at [ 0 , ' colors ' ] ) and df . at [ 0 , ' colors ' ] . strip ( ) :
self . colors = [ color . strip ( ) for color in df . at [ 0 , ' colors ' ] . split ( ' , ' ) if color . strip ( ) ]
if not self . colors :
self . colors = [ ' COLORLESS ' ]
else :
self . colors = [ ' COLORLESS ' ]
2024-12-30 11:43:36 -08:00
2024-12-06 12:04:39 -08:00
# Set creature types
2024-12-19 12:02:26 -08:00
self . creature_types = str ( df . at [ 0 , ' creatureTypes ' ] )
2024-12-30 11:43:36 -08:00
2024-12-19 12:02:26 -08:00
# Set deck theme tags
self . commander_tags = list ( df . at [ 0 , ' themeTags ' ] )
2024-12-30 11:43:36 -08:00
2024-12-19 12:02:26 -08:00
self . determine_themes ( )
2024-12-30 11:43:36 -08:00
2024-12-19 12:02:26 -08:00
self . commander_dict = {
' Commander Name ' : self . commander ,
' Mana Cost ' : self . commander_mana_cost ,
2024-12-29 20:16:57 -08:00
' Mana Value ' : self . commander_mana_value ,
2024-12-20 07:10:53 -08:00
' Color Identity ' : self . color_identity_full ,
2024-12-19 12:02:26 -08:00
' Colors ' : self . colors ,
' Type ' : self . commander_type ,
' Creature Types ' : self . creature_types ,
' Text ' : self . commander_text ,
' Power ' : self . commander_power ,
' Toughness ' : self . commander_toughness ,
' Themes ' : self . themes
}
2024-12-29 20:16:57 -08:00
self . add_card ( self . commander , self . commander_type , self . commander_mana_cost , self . commander_mana_value , True )
2024-12-19 12:02:26 -08:00
# Begin Building the Deck
2024-12-26 11:59:30 -08:00
self . setup_dataframes ( )
2024-12-19 12:02:26 -08:00
self . determine_ideals ( )
self . add_lands ( )
2024-12-27 12:02:50 -08:00
self . add_creatures ( )
2024-12-29 20:16:57 -08:00
self . add_ramp ( )
2024-12-31 09:18:02 -08:00
self . add_board_wipes ( )
2024-12-25 19:56:35 -08:00
self . add_interaction ( )
self . add_card_advantage ( )
2024-12-29 20:16:57 -08:00
if len ( self . card_library ) < 100 :
self . fill_out_deck ( )
2024-12-25 19:56:35 -08:00
self . card_library . to_csv ( f ' { csv_directory } /test_deck_presort.csv ' , index = False )
self . organize_library ( )
2024-12-29 20:16:57 -08:00
self . card_library . to_csv ( f ' { csv_directory } /test_deck_preconcat.csv ' , index = False )
2024-12-31 09:18:02 -08:00
logging . info ( f ' Creature cards (including commander): { self . creature_cards } ' )
logging . info ( f ' Planeswalker cards: { self . planeswalker_cards } ' )
logging . info ( f ' Battle cards: { self . battle_cards } ' )
logging . info ( f ' Instant cards: { self . instant_cards } ' )
logging . info ( f ' Sorcery cards: { self . sorcery_cards } ' )
logging . info ( f ' Artifact cards: { self . artifact_cards } ' )
logging . info ( f ' Enchantment cards: { self . enchantment_cards } ' )
logging . info ( f ' Land cards cards: { self . land_cards } ' )
logging . info ( f ' Number of cards in Library: { len ( self . card_library ) } ' )
2024-12-30 11:43:36 -08:00
self . get_cmc ( )
self . count_pips ( )
2024-12-25 19:56:35 -08:00
self . concatenate_duplicates ( )
2024-12-27 12:02:50 -08:00
self . organize_library ( )
2024-12-29 20:16:57 -08:00
self . sort_library ( )
self . commander_to_top ( )
2024-12-25 19:56:35 -08:00
self . card_library . to_csv ( f ' { csv_directory } /test_deck_done.csv ' , index = False )
self . full_df . to_csv ( f ' { csv_directory } /test_all_after_done.csv ' , index = False )
2024-12-29 20:16:57 -08:00
2024-12-31 10:16:47 -08:00
def determine_color_identity ( self ) - > None :
""" Determine the deck ' s color identity and set related attributes. """
# Single color mapping
mono_color_map = {
' COLORLESS ' : ( ' Colorless ' , [ ' colorless ' ] ) ,
' B ' : ( ' Black ' , [ ' colorless ' , ' black ' ] ) ,
' G ' : ( ' Green ' , [ ' colorless ' , ' green ' ] ) ,
' R ' : ( ' Red ' , [ ' colorless ' , ' red ' ] ) ,
' U ' : ( ' Blue ' , [ ' colorless ' , ' blue ' ] ) ,
' w ' : ( ' White ' , [ ' colorless ' , ' white ' ] )
}
# Two-color mapping
dual_color_map = {
' B, G ' : ( ' Golgari: Black/Green ' , [ ' B ' , ' G ' , ' B, G ' ] , [ ' colorless ' , ' black ' , ' green ' , ' golgari ' ] ) ,
' B, R ' : ( ' Rakdos: Black/Red ' , [ ' B ' , ' R ' , ' B, R ' ] , [ ' colorless ' , ' black ' , ' red ' , ' rakdos ' ] ) ,
' B, U ' : ( ' Dimir: Black/Blue ' , [ ' B ' , ' U ' , ' B, U ' ] , [ ' colorless ' , ' black ' , ' blue ' , ' dimir ' ] ) ,
' B, W ' : ( ' Orzhov: Black/White ' , [ ' B ' , ' W ' , ' B, W ' ] , [ ' colorless ' , ' black ' , ' white ' , ' orzhov ' ] ) ,
' G, R ' : ( ' Gruul: Green/Red ' , [ ' G ' , ' R ' , ' G, R ' ] , [ ' colorless ' , ' green ' , ' red ' , ' gruul ' ] ) ,
' G, U ' : ( ' Simic: Green/Blue ' , [ ' G ' , ' U ' , ' G, U ' ] , [ ' colorless ' , ' green ' , ' blue ' , ' simic ' ] ) ,
' G, W ' : ( ' Selesnya: Green/White ' , [ ' G ' , ' W ' , ' G, W ' ] , [ ' colorless ' , ' green ' , ' white ' , ' selesnya ' ] ) ,
' R, U ' : ( ' Izzet: Blue/Red ' , [ ' U ' , ' R ' , ' U, R ' ] , [ ' colorless ' , ' blue ' , ' red ' , ' izzet ' ] ) ,
' U, W ' : ( ' Azorius: Blue/White ' , [ ' U ' , ' W ' , ' U, W ' ] , [ ' colorless ' , ' blue ' , ' white ' , ' azorius ' ] ) ,
' R, W ' : ( ' Boros: Red/White ' , [ ' R ' , ' W ' , ' R, W ' ] , [ ' colorless ' , ' red ' , ' white ' , ' boros ' ] )
}
# Three-color mapping
tri_color_map = {
' B, G, U ' : ( ' Sultai: Black/Blue/Green ' , [ ' B ' , ' G ' , ' U ' , ' B, G ' , ' B, U ' , ' G, U ' , ' B, G, U ' ] ,
[ ' colorless ' , ' black ' , ' blue ' , ' green ' , ' dimir ' , ' golgari ' , ' simic ' , ' sultai ' ] ) ,
' B, G, R ' : ( ' Jund: Black/Red/Green ' , [ ' B ' , ' G ' , ' R ' , ' B, G ' , ' B, R ' , ' G, R ' , ' B, G, R ' ] ,
[ ' colorless ' , ' black ' , ' green ' , ' red ' , ' golgari ' , ' rakdos ' , ' gruul ' , ' jund ' ] ) ,
' B, G, W ' : ( ' Abzan: Black/Green/White ' , [ ' B ' , ' G ' , ' W ' , ' B, G ' , ' B, W ' , ' G, W ' , ' B, G, W ' ] ,
[ ' colorless ' , ' black ' , ' green ' , ' white ' , ' golgari ' , ' orzhov ' , ' selesnya ' , ' abzan ' ] ) ,
' B, R, U ' : ( ' Grixis: Black/Blue/Red ' , [ ' B ' , ' R ' , ' U ' , ' B, R ' , ' B, U ' , ' R, U ' , ' B, R, U ' ] ,
[ ' colorless ' , ' black ' , ' blue ' , ' red ' , ' dimir ' , ' rakdos ' , ' izzet ' , ' grixis ' ] ) ,
' B, R, W ' : ( ' Mardu: Black/Red/White ' , [ ' B ' , ' R ' , ' W ' , ' B, R ' , ' B, W ' , ' R, W ' , ' B, R, W ' ] ,
[ ' colorless ' , ' black ' , ' red ' , ' white ' , ' rakdos ' , ' orzhov ' , ' boros ' , ' mardu ' ] ) ,
' B, U, W ' : ( ' Esper: Black/Blue/White ' , [ ' B ' , ' U ' , ' W ' , ' B, U ' , ' B, W ' , ' U, W ' , ' B, U, W ' ] ,
[ ' colorless ' , ' black ' , ' blue ' , ' white ' , ' dimir ' , ' orzhov ' , ' azorius ' , ' esper ' ] ) ,
' G, R, U ' : ( ' Temur: Blue/Green/Red ' , [ ' G ' , ' R ' , ' U ' , ' G, R ' , ' G, U ' , ' R, U ' , ' G, R, U ' ] ,
[ ' colorless ' , ' green ' , ' red ' , ' blue ' , ' simic ' , ' izzet ' , ' gruul ' , ' temur ' ] ) ,
' G, R, W ' : ( ' Naya: Green/Red/White ' , [ ' G ' , ' R ' , ' W ' , ' G, R ' , ' G, W ' , ' R, W ' , ' G, R, W ' ] ,
[ ' green ' , ' red ' , ' white ' , ' gruul ' , ' selesnya ' , ' boros ' , ' naya ' ] ) ,
' G, U, W ' : ( ' Bant: Blue/Green/White ' , [ ' G ' , ' U ' , ' W ' , ' G, U ' , ' G, W ' , ' U, W ' , ' G, U, W ' ] ,
[ ' colorless ' , ' green ' , ' blue ' , ' white ' , ' simic ' , ' azorius ' , ' selesnya ' , ' bant ' ] ) ,
' R, U, W ' : ( ' Jeskai: Blue/Red/White ' , [ ' R ' , ' U ' , ' W ' , ' R, U ' , ' U, W ' , ' R, W ' , ' R, U, W ' ] ,
[ ' colorless ' , ' blue ' , ' red ' , ' white ' , ' izzet ' , ' azorius ' , ' boros ' , ' jeskai ' ] )
}
other_color_map = {
' B, G, R, U ' : ( ' Glint: Black/Blue/Green/Red ' ,
[ ' B ' , ' G ' , ' R ' , ' U ' , ' B, G ' , ' B, R ' , ' B, U ' , ' G, R ' , ' G, U ' , ' R, U ' , ' B, G, R ' ,
' B, G, U ' , ' B, R, U ' , ' G, R, U ' , ' B, G, R, U ' ] ,
[ ' colorless ' , ' black ' , ' blue ' , ' green ' , ' red ' , ' golgari ' , ' rakdos ' , ' dimir ' ,
' gruul ' , ' simic ' , ' izzet ' , ' jund ' , ' sultai ' , ' grixis ' , ' temur ' , ' glint ' ] ) ,
' B, G, R, W ' : ( ' Dune: Black/Green/Red/White ' ,
[ ' B ' , ' G ' , ' R ' , ' W ' , ' B, G ' , ' B, R ' , ' B, W ' , ' G, R ' , ' G, W ' , ' R, W ' , ' B, G, R ' ,
' B, G, W ' , ' B, R, W ' , ' G, R, W ' , ' B, G, R, W ' ] ,
[ ' colorless ' , ' black ' , ' green ' , ' red ' , ' white ' , ' golgari ' , ' rakdos ' , ' orzhov ' ,
' gruul ' , ' selesnya ' , ' boros ' , ' jund ' , ' abzan ' , ' mardu ' , ' naya ' , ' dune ' ] ) ,
' B, G, U, W ' : ( ' Witch: Black/Blue/Green/White ' ,
[ ' B ' , ' G ' , ' U ' , ' W ' , ' B, G ' , ' B, U ' , ' B, W ' , ' G, U ' , ' G, W ' , ' U, W ' , ' B, G, U ' ,
' B, G, W ' , ' B, U, W ' , ' G, U, W ' , ' B, G, U, W ' ] ,
[ ' colorless ' , ' black ' , ' blue ' , ' green ' , ' white ' , ' golgari ' , ' dimir ' , ' orzhov ' ,
' simic ' , ' selesnya ' , ' azorius ' , ' sultai ' , ' abzan ' , ' esper ' , ' bant ' , ' witch ' ] ) ,
' B, R, U, W ' : ( ' Yore: Black/Blue/Red/White ' ,
[ ' B ' , ' R ' , ' U ' , ' W ' , ' B, R ' , ' B, U ' , ' B, W ' , ' R, U ' , ' R, W ' , ' U, W ' , ' B, R, U ' ,
' B, R, W ' , ' B, U, W ' , ' R, U, W ' , ' B, R, U, W ' ] ,
[ ' colorless ' , ' black ' , ' blue ' , ' red ' , ' white ' , ' rakdos ' , ' dimir ' , ' orzhov ' ,
' izzet ' , ' boros ' , ' azorius ' , ' grixis ' , ' mardu ' , ' esper ' , ' mardu ' , ' yore ' ] ) ,
' G, R, U, W ' : ( ' Ink: Blue/Green/Red/White ' ,
[ ' G ' , ' R ' , ' U ' , ' W ' , ' G, R ' , ' G, U ' , ' G, W ' , ' R, U ' , ' R, W ' , ' U, W ' , ' G, R, U ' ,
' G, R, W ' , ' G, U, W ' , ' R, U, W ' , ' G, R, U, W ' ] ,
[ ' colorless ' , ' blue ' , ' green ' , ' red ' , ' white ' , ' gruul ' , ' simic ' , ' selesnya ' ,
' izzet ' , ' boros ' , ' azorius ' , ' temur ' , ' naya ' , ' bant ' , ' jeskai ' , ' ink ' ] ) ,
' B, G, R, U, W ' : ( ' WUBRG: All colors ' ,
[ ' B ' , ' G ' , ' R ' , ' U ' , ' W ' , ' B, G ' , ' B, R ' , ' B, U ' , ' B, W ' , ' G, R ' , ' G, U ' ,
' G, W ' , ' R, U ' , ' R, W ' , ' U, W ' , ' B, G, R ' , ' B, G, U ' , ' B, G, W ' , ' B, R, U ' ,
' B, R, W ' , ' B, U, W ' , ' G, R, U ' , ' G, R, W ' , ' B, U ,W ' , ' R, U, W ' ,
' B, G, R, U ' , ' B, G, R, W ' , ' B, G, U, W ' , ' B, R, U, W ' , ' G, R, U, W ' ,
' B, G, R, U, W ' ] ,
[ ' colorless ' , ' black ' , ' green ' , ' red ' , ' blue ' , ' white ' , ' golgari ' , ' rakdos ' ,
' dimir ' , ' orzhov ' , ' gruul ' , ' simic ' , ' selesnya ' , ' izzet ' , ' boros ' , ' azorius ' ,
' jund ' , ' sultai ' , ' abzan ' , ' grixis ' , ' mardu ' , ' esper ' , ' temur ' , ' naya ' ,
2024-12-31 10:48:11 -08:00
' bant ' , ' jeskai ' , ' glint ' , ' dune ' , ' witch ' , ' yore ' , ' ink ' , ' wubrg ' ] )
2024-12-31 10:16:47 -08:00
}
try :
# Handle mono-color identities
if self . color_identity in mono_color_map :
self . color_identity_full , self . files_to_load = mono_color_map [ self . color_identity ]
return
# Handle two-color identities
if self . color_identity in dual_color_map :
identity_info = dual_color_map [ self . color_identity ]
self . color_identity_full = identity_info [ 0 ]
self . color_identity_options = identity_info [ 1 ]
self . files_to_load = identity_info [ 2 ]
return
# Handle three-color identities
if self . color_identity in tri_color_map :
identity_info = tri_color_map [ self . color_identity ]
self . color_identity_full = identity_info [ 0 ]
self . color_identity_options = identity_info [ 1 ]
self . files_to_load = identity_info [ 2 ]
return
# Handle four-color/five-color identities
if self . color_identity in other_color_map :
identity_info = other_color_map [ self . color_identity ]
self . color_identity_full = identity_info [ 0 ]
self . color_identity_options = identity_info [ 1 ]
self . files_to_load = identity_info [ 2 ]
return
# If we get here, it's an unknown color identity
logging . warning ( f " Unknown color identity: { self . color_identity } " )
self . color_identity_full = ' Unknown '
2024-12-19 12:02:26 -08:00
self . files_to_load = [ ' colorless ' ]
2024-12-31 10:16:47 -08:00
except Exception as e :
logging . error ( f " Error in determine_color_identity: { e } " )
raise
2024-12-19 12:02:26 -08:00
2024-12-31 10:48:11 -08:00
def read_csv ( self , filename : str , converters : dict | None = None ) - > pd . DataFrame :
""" Read CSV file with error handling and logging.
Args :
filename : Name of the CSV file without extension
converters : Dictionary of converters for specific columns
Returns :
DataFrame from CSV file
"""
try :
filepath = f ' { csv_directory } / { filename } _cards.csv '
df = pd . read_csv ( filepath , converters = converters or { ' themeTags ' : pd . eval , ' creatureTypes ' : pd . eval } )
logging . debug ( f " Successfully read { filename } _cards.csv " )
return df
except FileNotFoundError as e :
logging . error ( f " File { filename } _cards.csv not found: { e } " )
raise
except Exception as e :
logging . error ( f " Error reading { filename } _cards.csv: { e } " )
raise
def write_csv ( self , df : pd . DataFrame , filename : str ) - > None :
""" Write DataFrame to CSV with error handling and logging.
Args :
df : DataFrame to write
filename : Name of the CSV file without extension
"""
try :
filepath = f ' { csv_directory } / { filename } .csv '
df . to_csv ( filepath , index = False )
logging . debug ( f " Successfully wrote { filename } .csv " )
except Exception as e :
logging . error ( f " Error writing { filename } .csv: { e } " )
raise
2024-12-25 19:56:35 -08:00
def setup_dataframes ( self ) :
2024-12-31 10:48:11 -08:00
""" Initialize and setup all required DataFrames. """
2024-12-25 19:56:35 -08:00
all_df = [ ]
for file in self . files_to_load :
2024-12-31 10:48:11 -08:00
df = self . read_csv ( file )
2024-12-25 19:56:35 -08:00
all_df . append ( df )
2024-12-31 10:48:11 -08:00
self . full_df = pd . concat ( all_df , ignore_index = True )
2024-12-25 19:56:35 -08:00
self . full_df . sort_values ( by = ' edhrecRank ' , inplace = True )
2024-12-26 11:59:30 -08:00
self . land_df = self . full_df [ self . full_df [ ' type ' ] . str . contains ( ' Land ' ) ] . copy ( )
self . land_df . sort_values ( by = ' edhrecRank ' , inplace = True )
self . land_df . to_csv ( f ' { csv_directory } /test_lands.csv ' , index = False )
self . full_df = self . full_df [ ~ self . full_df [ ' type ' ] . str . contains ( ' Land ' ) ]
self . full_df . to_csv ( f ' { csv_directory } /test_all.csv ' , index = False )
2024-12-25 19:56:35 -08:00
self . artifact_df = self . full_df [ self . full_df [ ' type ' ] . str . contains ( ' Artifact ' ) ] . copy ( )
self . artifact_df . sort_values ( by = ' edhrecRank ' , inplace = True )
self . artifact_df . to_csv ( f ' { csv_directory } /test_artifacts.csv ' , index = False )
self . battle_df = self . full_df [ self . full_df [ ' type ' ] . str . contains ( ' Battle ' ) ] . copy ( )
self . battle_df . sort_values ( by = ' edhrecRank ' , inplace = True )
self . battle_df . to_csv ( f ' { csv_directory } /test_battles.csv ' , index = False )
self . creature_df = self . full_df [ self . full_df [ ' type ' ] . str . contains ( ' Creature ' ) ] . copy ( )
self . creature_df . sort_values ( by = ' edhrecRank ' , inplace = True )
self . creature_df . to_csv ( f ' { csv_directory } /test_creatures.csv ' , index = False )
2024-12-27 12:02:50 -08:00
self . noncreature_df = self . full_df [ ~ self . full_df [ ' type ' ] . str . contains ( ' Creature ' ) ] . copy ( )
self . noncreature_df . sort_values ( by = ' edhrecRank ' , inplace = True )
self . noncreature_df . to_csv ( f ' { csv_directory } /test_noncreatures.csv ' , index = False )
2024-12-31 09:18:02 -08:00
self . noncreature_nonplaneswaker_df = self . noncreature_df [ ~ self . noncreature_df [ ' type ' ] . str . contains ( ' Planeswalker ' ) ] . copy ( )
self . noncreature_nonplaneswaker_df . sort_values ( by = ' edhrecRank ' , inplace = True )
self . noncreature_nonplaneswaker_df . to_csv ( f ' { csv_directory } /test_noncreatures.csv ' , index = False )
2024-12-25 19:56:35 -08:00
self . enchantment_df = self . full_df [ self . full_df [ ' type ' ] . str . contains ( ' Enchantment ' ) ] . copy ( )
self . enchantment_df . sort_values ( by = ' edhrecRank ' , inplace = True )
self . enchantment_df . to_csv ( f ' { csv_directory } /test_enchantments.csv ' , index = False )
self . instant_df = self . full_df [ self . full_df [ ' type ' ] . str . contains ( ' Instant ' ) ] . copy ( )
self . instant_df . sort_values ( by = ' edhrecRank ' , inplace = True )
self . instant_df . to_csv ( f ' { csv_directory } /test_instants.csv ' , index = False )
self . planeswalker_df = self . full_df [ self . full_df [ ' type ' ] . str . contains ( ' Planeswalker ' ) ] . copy ( )
self . planeswalker_df . sort_values ( by = ' edhrecRank ' , inplace = True )
self . planeswalker_df . to_csv ( f ' { csv_directory } /test_planeswalkers.csv ' , index = False )
self . sorcery_df = self . full_df [ self . full_df [ ' type ' ] . str . contains ( ' Sorcery ' ) ] . copy ( )
self . sorcery_df . sort_values ( by = ' edhrecRank ' , inplace = True )
self . sorcery_df . to_csv ( f ' { csv_directory } /test_sorcerys.csv ' , index = False )
2024-12-29 20:16:57 -08:00
2024-12-19 12:02:26 -08:00
def determine_themes ( self ) :
themes = self . commander_tags
print ( ' Your commander deck will likely have a number of viable themes, but you \' ll want to narrow it down for focus. \n '
2024-12-20 13:39:23 -08:00
' This will go through the process of choosing up to three themes for the deck. \n ' )
2024-12-19 12:02:26 -08:00
while True :
# Choose a primary theme
print ( ' Choose a primary theme for your commander deck. \n '
2024-12-20 13:39:23 -08:00
' This will be the " focus " of the deck, in a kindred deck this will typically be a creature type for example. ' )
2024-12-26 11:59:30 -08:00
choice = self . questionnaire ( ' Choice ' , choices_list = themes )
2024-12-19 12:02:26 -08:00
self . primary_theme = choice
2024-12-30 11:43:36 -08:00
weights_default = {
' primary ' : 1.0 ,
' secondary ' : 0.0 ,
' tertiary ' : 0.0 ,
' hidden ' : 0.0
}
2024-12-31 09:18:02 -08:00
weights = weights_default . copy ( )
2024-12-19 12:02:26 -08:00
themes . remove ( choice )
themes . append ( ' Stop Here ' )
2024-12-31 10:16:47 -08:00
self . primary_weight = weights [ ' primary ' ]
2024-12-27 12:02:50 -08:00
2024-12-19 12:02:26 -08:00
secondary_theme_chosen = False
tertiary_theme_chosen = False
2024-12-30 11:43:36 -08:00
self . hidden_theme = False
2024-12-27 12:02:50 -08:00
2024-12-19 12:02:26 -08:00
while not secondary_theme_chosen :
# Secondary theme
print ( ' Choose a secondary theme for your commander deck. \n '
' This will typically be a secondary focus, like card draw for Spellslinger, or +1/+1 counters for Aggro. ' )
2024-12-26 11:59:30 -08:00
choice = self . questionnaire ( ' Choice ' , choices_list = themes )
2024-12-19 12:02:26 -08:00
while True :
if choice == ' Stop Here ' :
2024-12-31 09:18:02 -08:00
logging . warning ( ' You \' ve only selected one theme, are you sure you want to stop? \n ' )
2024-12-27 12:02:50 -08:00
confirm_done = self . questionnaire ( ' Confirm ' , False )
2024-12-19 12:02:26 -08:00
if confirm_done :
secondary_theme_chosen = True
self . secondary_theme = False
tertiary_theme_chosen = True
self . tertiary_theme = False
themes . remove ( choice )
break
else :
pass
else :
2024-12-31 09:18:02 -08:00
weights = weights_default . copy ( ) # primary = 1.0, secondary = 0.0, tertiary = 0.0
2024-12-19 12:02:26 -08:00
self . secondary_theme = choice
themes . remove ( choice )
secondary_theme_chosen = True
2024-12-27 12:02:50 -08:00
# Set weights for primary/secondary themes
2024-12-30 11:43:36 -08:00
if ' Kindred ' in self . primary_theme and ' Kindred ' not in self . secondary_theme :
2024-12-31 10:16:47 -08:00
weights [ ' primary ' ] - = 0.1 # 0.8
weights [ ' secondary ' ] + = 0.1 # 0.1
2024-12-27 12:02:50 -08:00
elif ' Kindred ' in self . primary_theme and ' Kindred ' in self . secondary_theme :
2024-12-31 10:16:47 -08:00
weights [ ' primary ' ] - = 0.7 # 0.7
weights [ ' secondary ' ] + = 0.3 # 0.3
else :
2024-12-30 11:43:36 -08:00
weights [ ' primary ' ] - = 0.4 # 0.6
weights [ ' secondary ' ] + = 0.4 # 0.4
2024-12-27 12:02:50 -08:00
self . primary_weight = weights [ ' primary ' ]
self . secondary_weight = weights [ ' secondary ' ]
2024-12-19 12:02:26 -08:00
break
2024-12-27 12:02:50 -08:00
2024-12-19 12:02:26 -08:00
while not tertiary_theme_chosen :
# Tertiary theme
2024-12-20 13:39:23 -08:00
print ( ' Choose a tertiary theme for your commander deck. \n '
2024-12-19 12:02:26 -08:00
' This will typically be a tertiary focus, or just something else to do that your commander is good at. ' )
2024-12-26 11:59:30 -08:00
choice = self . questionnaire ( ' Choice ' , choices_list = themes )
2024-12-19 12:02:26 -08:00
while True :
if choice == ' Stop Here ' :
2024-12-31 09:18:02 -08:00
logging . warning ( ' You \' ve only selected two themes, are you sure you want to stop? \n ' )
2024-12-20 22:02:35 -08:00
confirm_done = self . questionnaire ( ' Confirm ' , False )
2024-12-19 12:02:26 -08:00
if confirm_done :
tertiary_theme_chosen = True
self . tertiary_theme = False
themes . remove ( choice )
break
else :
pass
else :
2024-12-31 09:18:02 -08:00
weights = weights_default . copy ( ) # primary = 1.0, secondary = 0.0, tertiary = 0.0
2024-12-19 12:02:26 -08:00
self . tertiary_theme = choice
tertiary_theme_chosen = True
2024-12-27 12:02:50 -08:00
2024-12-30 11:43:36 -08:00
# Set weights for themes:
if ' Kindred ' in self . primary_theme and ' Kindred ' not in self . secondary_theme and ' Kindred ' not in self . tertiary_theme :
2024-12-31 09:18:02 -08:00
weights [ ' primary ' ] - = 0.2 # 0.8
weights [ ' secondary ' ] + = 0.1 # 0.1
2024-12-30 11:43:36 -08:00
weights [ ' tertiary ' ] + = 0.1 # 0.1
elif ' Kindred ' in self . primary_theme and ' Kindred ' in self . secondary_theme and ' Kindred ' not in self . tertiary_theme :
2024-12-31 10:16:47 -08:00
weights [ ' primary ' ] - = 0.3 # 0.7
weights [ ' secondary ' ] + = 0.2 # 0.2
2024-12-30 11:43:36 -08:00
weights [ ' tertiary ' ] + = 0.1 # 0.1
2024-12-27 12:02:50 -08:00
elif ' Kindred ' in self . primary_theme and ' Kindred ' in self . secondary_theme and ' Kindred ' in self . tertiary_theme :
2024-12-30 11:43:36 -08:00
weights [ ' primary ' ] - = 0.5 # 0.5
weights [ ' secondary ' ] + = 0.3 # 0.3
weights [ ' tertiary ' ] + = 0.2 # 0.2
2024-12-27 12:02:50 -08:00
else :
2024-12-30 11:43:36 -08:00
weights [ ' primary ' ] - = 0.6 # 0.4
weights [ ' secondary ' ] + = 0.3 # 0.3
weights [ ' tertiary ' ] + = 0.3 # 0.3
2024-12-27 12:02:50 -08:00
self . primary_weight = weights [ ' primary ' ]
self . secondary_weight = weights [ ' secondary ' ]
self . tertiary_weight = weights [ ' tertiary ' ]
2024-12-19 12:02:26 -08:00
break
2024-12-30 11:43:36 -08:00
self . themes = [ self . primary_theme ]
if not self . secondary_theme :
pass
else :
self . themes . append ( self . secondary_theme )
if not self . tertiary_theme :
pass
else :
self . themes . append ( self . tertiary_theme )
"""
Setting ' Hidden ' themes for multiple - copy cards , such as ' Hare Apparent ' or ' Shadowborn Apostle ' .
These are themes that will be prompted for under specific conditions , such as a matching Kindred theme or a matching color combination and Spellslinger theme for example .
Typically a hidden theme won ' t come up, but if it does, it will take priority with theme weights to ensure a decent number of the specialty cards are added.
"""
# Setting hidden theme for Kindred-specific themes
hidden_themes = [ ' Advisor Kindred ' , ' Demon Kindred ' , ' Dwarf Kindred ' , ' Rabbit Kindred ' , ' Rat Kindred ' , ' Wraith Kindred ' ]
theme_cards = [ ' Persistent Petitioners ' , ' Shadowborn Apostle ' , ' Seven Dwarves ' , ' Hare Apparent ' , [ ' Rat Colony ' , ' Relentless Rats ' ] , ' Nazgûl ' ]
2024-12-30 12:06:46 -08:00
color = [ ' B ' , ' B ' , ' R ' , ' W ' , ' B ' , ' B ' ]
2024-12-30 11:43:36 -08:00
for i in range ( min ( len ( hidden_themes ) , len ( theme_cards ) , len ( color ) ) ) :
if ( hidden_themes [ i ] in self . themes
and hidden_themes [ i ] != ' Rat Kindred '
and color [ i ] in self . colors ) :
2024-12-31 09:18:02 -08:00
logging . info ( f ' Looks like you \' re making a { hidden_themes [ i ] } deck, would you like it to be a { theme_cards [ i ] } deck? ' )
2024-12-30 11:43:36 -08:00
choice = self . questionnaire ( ' Confirm ' , False )
if choice :
self . hidden_theme = theme_cards [ i ]
self . themes . append ( self . hidden_theme )
2024-12-31 09:18:02 -08:00
weights [ ' primary ' ] = round ( weights [ ' primary ' ] / 3 , 2 )
weights [ ' secondary ' ] = round ( weights [ ' secondary ' ] / 2 , 2 )
weights [ ' tertiary ' ] = weights [ ' tertiary ' ]
weights [ ' hidden ' ] = round ( 1.0 - weights [ ' primary ' ] - weights [ ' secondary ' ] - weights [ ' tertiary ' ] , 2 )
2024-12-30 11:43:36 -08:00
self . primary_weight = weights [ ' primary ' ]
self . secondary_weight = weights [ ' secondary ' ]
self . tertiary_weight = weights [ ' tertiary ' ]
self . hidden_weight = weights [ ' hidden ' ]
else :
continue
elif ( hidden_themes [ i ] in self . themes
and hidden_themes [ i ] == ' Rat Kindred '
and color [ i ] in self . colors ) :
2024-12-31 09:18:02 -08:00
logging . info ( f ' Looks like you \' re making a { hidden_themes [ i ] } deck, would you like it to be a { theme_cards [ i ] [ 0 ] } or { theme_cards [ i ] [ 1 ] } deck? ' )
2024-12-30 11:43:36 -08:00
choice = self . questionnaire ( ' Confirm ' , False )
if choice :
print ( ' Which one? ' )
choice = self . questionnaire ( ' Choice ' , choices_list = theme_cards [ i ] )
if choice :
self . hidden_theme = choice
self . themes . append ( self . hidden_theme )
2024-12-31 09:18:02 -08:00
weights [ ' primary ' ] = round ( weights [ ' primary ' ] / 3 , 2 )
weights [ ' secondary ' ] = round ( weights [ ' secondary ' ] / 2 , 2 )
weights [ ' tertiary ' ] = weights [ ' tertiary ' ]
weights [ ' hidden ' ] = round ( 1.0 - weights [ ' primary ' ] - weights [ ' secondary ' ] - weights [ ' tertiary ' ] , 2 )
2024-12-30 11:43:36 -08:00
self . primary_weight = weights [ ' primary ' ]
self . secondary_weight = weights [ ' secondary ' ]
self . tertiary_weight = weights [ ' tertiary ' ]
self . hidden_weight = weights [ ' hidden ' ]
else :
continue
# Setting the hidden theme for non-Kindred themes
2024-12-30 12:06:46 -08:00
hidden_themes = [ ' Little Fellas ' , ' Mill ' , ' Spellslinger ' , ' Spells Matter ' , ' Spellslinger ' , ' Spells Matter ' , ]
theme_cards = [ ' Hare Apparent ' , ' Persistent Petitions ' , ' Dragon \' s Approach ' , ' Dragon \' s Approach ' , ' Slime Against Humanity ' , ' Slime Against Humanity ' ]
color = [ ' W ' , ' B ' , ' R ' , ' R ' , ' G ' , ' G ' ]
2024-12-30 11:43:36 -08:00
for i in range ( min ( len ( hidden_themes ) , len ( theme_cards ) , len ( color ) ) ) :
if ( hidden_themes [ i ] in self . themes
and color [ i ] in self . colors ) :
2024-12-31 09:18:02 -08:00
logging . info ( f ' Looks like you \' re making a { hidden_themes [ i ] } deck, would you like it to be a { theme_cards [ i ] } deck? ' )
2024-12-30 11:43:36 -08:00
choice = self . questionnaire ( ' Confirm ' , False )
if choice :
self . hidden_theme = theme_cards [ i ]
self . themes . append ( self . hidden_theme )
2024-12-31 09:18:02 -08:00
weights [ ' primary ' ] = round ( weights [ ' primary ' ] / 3 , 2 )
weights [ ' secondary ' ] = round ( weights [ ' secondary ' ] / 2 , 2 )
weights [ ' tertiary ' ] = weights [ ' tertiary ' ]
weights [ ' hidden ' ] = round ( 1.0 - weights [ ' primary ' ] - weights [ ' secondary ' ] - weights [ ' tertiary ' ] , 2 )
2024-12-30 11:43:36 -08:00
self . primary_weight = weights [ ' primary ' ]
self . secondary_weight = weights [ ' secondary ' ]
self . tertiary_weight = weights [ ' tertiary ' ]
self . hidden_weight = weights [ ' hidden ' ]
else :
continue
2024-12-31 09:18:02 -08:00
2024-12-19 12:02:26 -08:00
break
2024-12-29 20:16:57 -08:00
2024-12-05 16:54:56 -08:00
def determine_ideals ( self ) :
2024-12-06 12:04:39 -08:00
# "Free" slots that can be used for anything that isn't the ideals
self . free_slots = 99
2024-12-20 13:39:23 -08:00
if use_scrython :
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. ' )
2024-12-20 22:02:35 -08:00
choice = self . questionnaire ( ' Confirm ' , False )
2024-12-20 13:39:23 -08:00
if choice :
self . set_max_deck_price = True
2024-12-21 00:58:12 -08:00
self . deck_cost = 0.0
2024-12-20 13:39:23 -08:00
print ( ' What would you like the max price to be? ' )
2024-12-27 12:02:50 -08:00
self . max_deck_price = float ( self . questionnaire ( ' Number ' , 400 ) )
2024-12-25 19:56:35 -08:00
new_line ( )
2024-12-20 13:39:23 -08:00
else :
self . set_max_deck_price = False
2024-12-25 19:56:35 -08:00
new_line ( )
2024-12-20 13:39:23 -08:00
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. ' )
2024-12-20 22:02:35 -08:00
choice = self . questionnaire ( ' Confirm ' , False )
2024-12-20 13:39:23 -08:00
if choice :
self . set_max_card_price = True
print ( ' What would you like the max price to be? ' )
2024-12-27 12:02:50 -08:00
answer = float ( self . questionnaire ( ' Number ' , 20 ) )
2024-12-20 22:02:35 -08:00
self . max_card_price = answer
2024-12-25 19:56:35 -08:00
self . card_library [ ' Card Price ' ] = pd . Series ( dtype = ' float ' )
new_line ( )
2024-12-20 13:39:23 -08:00
else :
self . set_max_card_price = False
2024-12-25 19:56:35 -08:00
new_line ( )
2024-12-20 13:39:23 -08:00
# Determine ramp
print ( ' How many pieces of ramp would you like to include? \n '
2024-12-27 12:02:50 -08:00
' This includes mana rocks, mana dorks, and land ramp spells. \n '
' A good baseline is 8-12 pieces, scaling up with higher average CMC \n '
' Default: 8 ' )
answer = self . questionnaire ( ' Number ' , 8 )
2024-12-20 22:02:35 -08:00
self . ideal_ramp = int ( answer )
2024-12-20 13:39:23 -08:00
self . free_slots - = self . ideal_ramp
2024-12-25 19:56:35 -08:00
new_line ( )
2024-12-20 13:39:23 -08:00
2024-12-05 16:54:56 -08:00
# Determine ideal land count
2024-12-27 12:02:50 -08:00
print ( ' How many total lands would you like to include? \n '
' Before ramp is considered, 38-40 lands is typical for most decks. \n '
" For landfall decks, consider starting at 40 lands before ramp. \n "
' As a guideline, each mana source from ramp can reduce land count by ~1. \n '
' Default: 35 ' )
answer = self . questionnaire ( ' Number ' , 35 )
2024-12-20 22:02:35 -08:00
self . ideal_land_count = int ( answer )
2024-12-06 12:04:39 -08:00
self . free_slots - = self . ideal_land_count
2024-12-25 19:56:35 -08:00
new_line ( )
2024-12-20 22:02:35 -08:00
# Determine minimum basics to have
print ( ' How many basic lands would you like to have at minimum? \n '
' This can vary widely depending on your commander, colors in color identity, and what you want to do. \n '
2024-12-27 12:02:50 -08:00
' Some decks may be fine with as low as 10, others may want 25. \n '
' Default: 20 ' )
answer = self . questionnaire ( ' Number ' , 20 )
2024-12-20 22:02:35 -08:00
self . min_basics = int ( answer )
2024-12-25 19:56:35 -08:00
new_line ( )
2024-12-20 22:02:35 -08:00
2024-12-06 12:04:39 -08:00
# Determine ideal creature count
print ( ' How many creatures would you like to include? \n '
' Something like 25-30 would be a good starting point. \n '
2024-12-27 12:02:50 -08:00
" If you ' re going for a kindred theme, going past 30 is likely normal. \n "
" Also be sure to take into account token generation, but remember you ' ll want enough to stay safe \n "
' Default: 25 ' )
answer = self . questionnaire ( ' Number ' , 25 )
2024-12-20 22:02:35 -08:00
self . ideal_creature_count = int ( answer )
2024-12-06 12:04:39 -08:00
self . free_slots - = self . ideal_creature_count
2024-12-25 19:56:35 -08:00
new_line ( )
2024-12-06 12:04:39 -08:00
# Determine spot/targetted removal
print ( ' How many spot removal pieces would you like to include? \n '
' A good starting point is about 8-12 pieces of spot removal. \n '
2024-12-30 11:43:36 -08:00
' Counterspells can be considered proactive removal and protection. \n '
2024-12-27 12:02:50 -08:00
' If you \' re going spellslinger, more would be a good idea as you might have less cretaures. \n '
' Default: 10 ' )
answer = self . questionnaire ( ' Number ' , 10 )
2024-12-20 22:02:35 -08:00
self . ideal_removal = int ( answer )
2024-12-06 12:04:39 -08:00
self . free_slots - = self . ideal_removal
2024-12-25 19:56:35 -08:00
new_line ( )
2024-12-06 12:04:39 -08:00
# Determine board wipes
2024-12-20 22:02:35 -08:00
print ( ' How many board wipes would you like to include? \n '
2024-12-06 12:04:39 -08:00
' Somewhere around 2-3 is good to help eliminate threats, but also prevent the game from running long \n . '
2024-12-27 12:02:50 -08:00
' This can include damaging wipes like " Blasphemous Act " or toughness reduction like " Meathook Massacre " . \n '
' Default: 2 ' )
answer = self . questionnaire ( ' Number ' , 2 )
2024-12-20 22:02:35 -08:00
self . ideal_wipes = int ( answer )
2024-12-06 12:04:39 -08:00
self . free_slots - = self . ideal_wipes
2024-12-25 19:56:35 -08:00
new_line ( )
2024-12-06 12:04:39 -08:00
# Determine card advantage
print ( ' How many pieces of card advantage would you like to include? \n '
' 10 pieces of card advantage is good, up to 14 is better. \n '
2024-12-27 12:02:50 -08:00
' Try to have a majority of it be non-conditional, and only have a couple of " Rhystic Study " style effects. \n '
' Default: 10 ' )
answer = self . questionnaire ( ' Number ' , 10 )
2024-12-20 22:02:35 -08:00
self . ideal_card_advantage = int ( answer )
2024-12-06 12:04:39 -08:00
self . free_slots - = self . ideal_card_advantage
2024-12-25 19:56:35 -08:00
new_line ( )
2024-12-06 12:04:39 -08:00
# Determine how many protection spells
2024-12-27 12:02:50 -08:00
print ( ' How many protection spells would you like to include? \n '
2024-12-06 12:04:39 -08:00
' This can be individual protection, board protection, fogs, or similar effects. \n '
2024-12-27 12:02:50 -08:00
' Things that grant indestructible, hexproof, phase out, or even just counterspells. \n '
' It \' s recommended to have 5 to 15, depending on your commander and preferred strategy. \n '
' Default: 8 ' )
answer = self . questionnaire ( ' Number ' , 8 )
2024-12-20 22:02:35 -08:00
self . ideal_protection = int ( answer )
2024-12-06 12:04:39 -08:00
self . free_slots - = self . ideal_protection
2024-12-25 19:56:35 -08:00
new_line ( )
2024-12-06 12:04:39 -08:00
print ( f ' Free slots that aren \' t part of the ideals: { self . free_slots } ' )
print ( ' Keep in mind that many of the ideals can also cover multiple roles, but this will give a baseline POV. ' )
2024-12-30 11:43:36 -08:00
def add_card ( self , card : str , card_type : str , mana_cost : str , mana_value : int , is_commander : bool = False ) - > None :
2024-12-27 12:02:50 -08:00
""" Add a card to the deck library with price checking if enabled.
2024-12-31 10:16:47 -08:00
2024-12-27 12:02:50 -08:00
Args :
2024-12-30 11:43:36 -08:00
card ( str ) : Name of the card to add
card_type ( str ) : Type of the card ( e . g . , ' Creature ' , ' Instant ' )
mana_cost ( str ) : Mana cost string representation
mana_value ( int ) : Converted mana cost / mana value
is_commander ( bool , optional ) : Whether this card is the commander . Defaults to False .
2024-12-31 10:16:47 -08:00
2024-12-30 11:43:36 -08:00
Returns :
None
2024-12-31 10:16:47 -08:00
2024-12-30 11:43:36 -08:00
Raises :
ValueError : If card price exceeds maximum allowed price when price checking is enabled
2024-12-27 12:02:50 -08:00
"""
2024-12-25 19:56:35 -08:00
multiple_copies = basic_lands + multiple_copy_cards
2024-12-27 12:02:50 -08:00
# Skip if card already exists and isn't allowed multiple copies
if card in pd . Series ( self . card_library [ ' Card Name ' ] ) . values and card not in multiple_copies :
return
# Handle price checking
card_price = 0.0
if use_scrython and self . set_max_card_price :
2024-12-31 10:16:47 -08:00
# Get price from cache or API
if card in self . card_prices :
card_price = self . card_prices [ card ]
else :
card_price = self . price_check ( card )
2024-12-27 12:02:50 -08:00
# Skip if card is too expensive
2024-12-31 10:16:47 -08:00
if card_price is not None and card_price > self . max_card_price * 1.1 :
2024-12-27 12:02:50 -08:00
logging . info ( f " Skipping { card } - price { card_price } exceeds maximum " )
return
# Create card entry
2024-12-29 20:16:57 -08:00
card_entry = [ card , card_type , mana_cost , mana_value , is_commander ]
2024-12-27 12:02:50 -08:00
if use_scrython and self . set_max_card_price :
card_entry . append ( card_price )
# Add to library
self . card_library . loc [ len ( self . card_library ) ] = card_entry
# Update deck cost if tracking
if self . set_max_deck_price :
self . deck_cost + = card_price
logging . debug ( f " Added { card } to deck library " )
2024-12-25 19:56:35 -08:00
def organize_library ( self ) :
2024-12-31 09:18:02 -08:00
# Initialize counters dictionary dynamically from card_types including Kindred
all_types = card_types + [ ' Kindred ' ] if ' Kindred ' not in card_types else card_types
card_counters = { card_type : 0 for card_type in all_types }
2024-12-30 11:43:36 -08:00
2024-12-27 12:02:50 -08:00
# Count cards by type
2024-12-25 19:56:35 -08:00
for card_type in card_types :
2024-12-27 12:02:50 -08:00
type_df = self . card_library [ self . card_library [ ' Card Type ' ] . apply ( lambda x : card_type in x ) ]
card_counters [ card_type ] = len ( type_df )
2024-12-30 11:43:36 -08:00
2024-12-27 12:02:50 -08:00
# Assign counts to instance variables
self . artifact_cards = card_counters [ ' Artifact ' ]
self . battle_cards = card_counters [ ' Battle ' ]
self . creature_cards = card_counters [ ' Creature ' ]
self . enchantment_cards = card_counters [ ' Enchantment ' ]
self . instant_cards = card_counters [ ' Instant ' ]
2024-12-31 09:18:02 -08:00
self . kindred_cards = card_counters . get ( ' Kindred ' , 0 ) # Use get() with default value
2024-12-27 12:02:50 -08:00
self . land_cards = card_counters [ ' Land ' ]
self . planeswalker_cards = card_counters [ ' Planeswalker ' ]
self . sorcery_cards = card_counters [ ' Sorcery ' ]
2024-12-29 20:16:57 -08:00
def sort_library ( self ) :
self . card_library [ ' Sort Order ' ] = pd . Series ( dtype = ' str ' )
for index , row in self . card_library . iterrows ( ) :
for card_type in card_types :
if card_type in row [ ' Card Type ' ] :
if row [ ' Sort Order ' ] == ' Creature ' :
continue
if row [ ' Sort Order ' ] != ' Creature ' :
self . card_library . loc [ index , ' Sort Order ' ] = card_type
2024-12-30 11:43:36 -08:00
2024-12-29 20:16:57 -08:00
custom_order = [ ' Planeswalker ' , ' Battle ' , ' Creature ' , ' Instant ' , ' Sorcery ' , ' Artifact ' , ' Enchantment ' , ' Land ' ]
2024-12-30 11:43:36 -08:00
self . card_library [ ' Sort Order ' ] = pd . Categorical (
self . card_library [ ' Sort Order ' ] ,
categories = custom_order ,
ordered = True
)
self . card_library = ( self . card_library
. sort_values ( by = [ ' Sort Order ' , ' Card Name ' ] , ascending = [ True , True ] )
. drop ( columns = [ ' Sort Order ' ] )
. reset_index ( drop = True )
)
2024-12-31 10:16:47 -08:00
def commander_to_top ( self ) - > None :
""" Move commander card to the top of the library while preserving commander status. """
2024-12-30 11:43:36 -08:00
try :
commander_row = self . card_library [ self . card_library [ ' Commander ' ] ] . copy ( )
if commander_row . empty :
logging . warning ( " No commander found in library " )
return
self . card_library = self . card_library [ ~ self . card_library [ ' Commander ' ] ]
self . card_library = pd . concat ( [ commander_row , self . card_library ] , ignore_index = True )
2024-12-31 10:16:47 -08:00
commander_name = commander_row [ ' Card Name ' ] . iloc [ 0 ]
logging . info ( f " Successfully moved commander ' { commander_name } ' to top " )
2024-12-30 11:43:36 -08:00
except Exception as e :
2024-12-31 10:16:47 -08:00
logging . error ( f " Error moving commander to top: { str ( e ) } " )
2024-12-25 19:56:35 -08:00
def concatenate_duplicates ( self ) :
2024-12-30 11:43:36 -08:00
""" Handle duplicate cards in the library while maintaining data integrity. """
2024-12-25 19:56:35 -08:00
duplicate_lists = basic_lands + multiple_copy_cards
2024-12-30 11:43:36 -08:00
# Create a count column for duplicates
self . card_library [ ' Card Count ' ] = 1
2024-12-25 19:56:35 -08:00
for duplicate in duplicate_lists :
2024-12-30 11:43:36 -08:00
mask = self . card_library [ ' Card Name ' ] == duplicate
count = mask . sum ( )
if count > 0 :
logging . info ( f ' Found { count } copies of { duplicate } ' )
# Keep first occurrence with updated count
first_idx = mask . idxmax ( )
self . card_library . loc [ first_idx , ' Card Count ' ] = count
# Drop other occurrences
self . card_library = self . card_library . drop (
self . card_library [ mask & ( self . card_library . index != first_idx ) ] . index
)
# Update card names with counts where applicable
mask = self . card_library [ ' Card Count ' ] > 1
self . card_library . loc [ mask , ' Card Name ' ] = (
self . card_library . loc [ mask , ' Card Name ' ] +
' x ' +
self . card_library . loc [ mask , ' Card Count ' ] . astype ( str )
)
# Clean up
self . card_library = self . card_library . drop ( columns = [ ' Card Count ' ] )
2024-12-29 20:16:57 -08:00
self . card_library = self . card_library . reset_index ( drop = True )
2024-12-30 11:43:36 -08:00
def drop_card ( self , dataframe : pd . DataFrame , index : int ) - > None :
""" Safely drop a card from the dataframe by index.
Args :
dataframe : DataFrame to modify
index : Index to drop
"""
2024-12-21 00:58:12 -08:00
try :
dataframe . drop ( index , inplace = True )
except KeyError :
2024-12-30 11:43:36 -08:00
logging . warning ( f " Attempted to drop non-existent index { index } " )
2024-12-06 12:04:39 -08:00
def add_lands ( self ) :
2024-12-27 12:02:50 -08:00
"""
2024-12-31 10:16:47 -08:00
Add lands to the deck based on ideal count and deck requirements .
The process follows these steps :
1. Add basic lands distributed by color identity
2. Add utility / staple lands
3. Add fetch lands if requested
4. Add theme - specific lands ( e . g . , Kindred )
5. Add multi - color lands based on color count
6. Add miscellaneous utility lands
7. Adjust total land count to match ideal count
2024-12-27 12:02:50 -08:00
"""
2024-12-31 10:16:47 -08:00
MAX_ADJUSTMENT_ATTEMPTS = 10
2024-12-21 00:58:12 -08:00
self . total_basics = 0
2024-12-19 12:02:26 -08:00
2024-12-31 10:16:47 -08:00
try :
# Add lands in sequence
self . add_basics ( )
self . check_basics ( )
self . add_standard_non_basics ( )
self . add_fetches ( )
# Add theme and color-specific lands
if any ( ' Kindred ' in theme for theme in self . themes ) :
self . add_kindred_lands ( )
if len ( self . colors ) > = 2 :
self . add_dual_lands ( )
if len ( self . colors ) > = 3 :
self . add_triple_lands ( )
self . add_misc_lands ( )
# Clean up land database
mask = self . land_df [ ' name ' ] . isin ( self . card_library [ ' Card Name ' ] )
self . land_df = self . land_df [ ~ mask ]
self . land_df . to_csv ( f ' { csv_directory } /test_lands.csv ' , index = False )
# Adjust to ideal land count
self . check_basics ( )
logging . info ( ' Adjusting total land count to match ideal count... ' )
2024-12-26 17:20:58 -08:00
self . organize_library ( )
2024-12-31 10:16:47 -08:00
attempts = 0
while self . land_cards > int ( self . ideal_land_count ) and attempts < MAX_ADJUSTMENT_ATTEMPTS :
logging . info ( f ' Current lands: { self . land_cards } , Target: { self . ideal_land_count } ' )
self . remove_basic ( )
self . organize_library ( )
attempts + = 1
if attempts > = MAX_ADJUSTMENT_ATTEMPTS :
logging . warning ( f " Could not reach ideal land count after { MAX_ADJUSTMENT_ATTEMPTS } attempts " )
logging . info ( f ' Final land count: { self . land_cards } ' )
except Exception as e :
logging . error ( f " Error during land addition: { e } " )
raise
2024-12-19 12:02:26 -08:00
def add_basics ( self ) :
2024-12-26 17:20:58 -08:00
base_basics = self . ideal_land_count - 10 # Reserve 10 slots for non-basic lands
2024-12-20 13:39:23 -08:00
basics_per_color = base_basics / / len ( self . colors )
remaining_basics = base_basics % len ( self . colors )
2024-12-31 09:18:02 -08:00
2024-12-20 13:39:23 -08:00
color_to_basic = {
' W ' : ' Plains ' ,
' U ' : ' Island ' ,
' B ' : ' Swamp ' ,
' R ' : ' Mountain ' ,
2024-12-27 12:02:50 -08:00
' G ' : ' Forest ' ,
' COLORLESS ' : ' Wastes '
2024-12-20 13:39:23 -08:00
}
2024-12-31 09:18:02 -08:00
2024-12-20 13:39:23 -08:00
if ' Snow ' in self . commander_tags :
color_to_basic = {
' W ' : ' Snow-Covered Plains ' ,
' U ' : ' Snow-Covered Island ' ,
' B ' : ' Snow-Covered Swamp ' ,
' R ' : ' Snow-Covered Mountain ' ,
2024-12-27 12:02:50 -08:00
' G ' : ' Snow-Covered Forest ' ,
' COLORLESS ' : ' Snow-Covered Wastes '
2024-12-20 13:39:23 -08:00
}
2024-12-31 09:18:02 -08:00
2024-12-20 13:39:23 -08:00
print ( f ' Adding { base_basics } basic lands distributed across { len ( self . colors ) } colors ' )
2024-12-31 09:18:02 -08:00
2024-12-20 13:39:23 -08:00
# Add equal distribution first
2024-12-19 12:02:26 -08:00
for color in self . colors :
2024-12-20 13:39:23 -08:00
basic = color_to_basic . get ( color )
if basic :
2024-12-31 09:18:02 -08:00
# Add basics with explicit commander flag and track count
2024-12-20 13:39:23 -08:00
for _ in range ( basics_per_color ) :
2024-12-31 09:18:02 -08:00
self . add_card ( basic , ' Basic Land ' , None , 0 , is_commander = False )
2024-12-20 13:39:23 -08:00
# Distribute remaining basics based on color requirements
if remaining_basics > 0 :
for color in self . colors [ : remaining_basics ] :
basic = color_to_basic . get ( color )
if basic :
2024-12-31 09:18:02 -08:00
self . add_card ( basic , ' Basic Land ' , None , 0 , is_commander = False )
2024-12-29 20:16:57 -08:00
lands_to_remove = [ ]
for key in color_to_basic :
basic = color_to_basic . get ( key )
lands_to_remove . append ( basic )
2024-12-31 09:18:02 -08:00
2024-12-29 20:16:57 -08:00
self . land_df = self . land_df [ ~ self . land_df [ ' name ' ] . isin ( lands_to_remove ) ]
self . land_df . to_csv ( f ' { csv_directory } /test_lands.csv ' , index = False )
2024-12-20 13:39:23 -08:00
2024-12-19 12:02:26 -08:00
def add_standard_non_basics ( self ) :
2024-12-31 10:16:47 -08:00
""" Add staple utility lands based on deck requirements. """
logging . info ( ' Adding staple non-basic lands ' )
# Define staple lands and their conditions
staple_lands = {
' Reliquary Tower ' : lambda : True , # Always include
' Ash Barrens ' : lambda : ' Landfall ' not in self . commander_tags ,
' Command Tower ' : lambda : len ( self . colors ) > 1 ,
' Exotic Orchard ' : lambda : len ( self . colors ) > 1 ,
' War Room ' : lambda : len ( self . colors ) < = 2 ,
' Rogue \' s Passage ' : lambda : self . commander_power > = 5
}
self . staples = [ ]
try :
# Add lands that meet their conditions
for land , condition in staple_lands . items ( ) :
if condition ( ) :
if land not in self . card_library [ ' Card Name ' ] . values :
self . add_card ( land , ' Land ' , None , 0 )
self . staples . append ( land )
logging . debug ( f " Added staple land: { land } " )
2024-12-19 12:02:26 -08:00
2024-12-31 10:16:47 -08:00
# Update land database
self . land_df = self . land_df [ ~ self . land_df [ ' name ' ] . isin ( self . staples ) ]
self . land_df . to_csv ( f ' { csv_directory } /test_lands.csv ' , index = False )
2024-12-19 12:02:26 -08:00
2024-12-31 10:16:47 -08:00
logging . info ( f ' Added { len ( self . staples ) } staple lands ' )
2024-12-21 00:58:12 -08:00
2024-12-31 10:16:47 -08:00
except Exception as e :
logging . error ( f " Error adding staple lands: { e } " )
raise
2024-12-20 07:10:53 -08:00
def add_fetches ( self ) :
# Determine how many fetches in total
print ( ' How many fetch lands would you like to include? \n '
' For most decks you \' ll likely be good with 3 or 4, just enough to thin the deck and help ensure the color availability. \n '
' If you \' re doing Landfall, more fetches would be recommended just to get as many Landfall triggers per turn. ' )
2024-12-27 12:02:50 -08:00
answer = self . questionnaire ( ' Number ' , 2 )
2024-12-30 11:43:36 -08:00
MAX_ATTEMPTS = 50 # Maximum attempts to prevent infinite loops
attempt_count = 0
2024-12-20 22:02:35 -08:00
desired_fetches = int ( answer )
2024-12-20 07:10:53 -08:00
chosen_fetches = [ ]
2024-12-30 11:43:36 -08:00
generic_fetches = [
' Evolving Wilds ' , ' Terramorphic Expanse ' , ' Shire Terrace ' ,
' Escape Tunnel ' , ' Promising Vein ' , ' Myriad Landscape ' ,
' Fabled Passage ' , ' Terminal Moraine '
]
fetches = generic_fetches . copy ( )
lands_to_remove = generic_fetches . copy ( )
2024-12-20 07:10:53 -08:00
2024-12-20 13:39:23 -08:00
# Adding in expensive fetches
2024-12-21 00:58:12 -08:00
if ( use_scrython and self . set_max_card_price ) :
2024-12-30 11:43:36 -08:00
if self . price_check ( ' Prismatic Vista ' ) < = self . max_card_price * 1.1 :
2024-12-27 12:02:50 -08:00
lands_to_remove . append ( ' Prismatic Vista ' )
2024-12-20 13:39:23 -08:00
fetches . append ( ' Prismatic Vista ' )
2024-12-20 22:02:35 -08:00
else :
2024-12-27 12:02:50 -08:00
lands_to_remove . append ( ' Prismatic Vista ' )
2024-12-20 22:02:35 -08:00
pass
2024-12-20 13:39:23 -08:00
else :
2024-12-27 12:02:50 -08:00
lands_to_remove . append ( ' Prismatic Vista ' )
2024-12-21 00:58:12 -08:00
fetches . append ( ' Prismatic Vista ' )
color_to_fetch = {
' 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 ' ]
}
2024-12-20 13:39:23 -08:00
2024-12-21 00:58:12 -08:00
for color in self . colors :
fetch = color_to_fetch . get ( color )
if fetch not in fetches :
fetches . extend ( fetch )
2024-12-27 12:02:50 -08:00
if fetch not in lands_to_remove :
lands_to_remove . extend ( fetch )
for color in color_to_fetch :
fetch = color_to_fetch . get ( color )
if fetch not in fetches :
fetches . extend ( fetch )
if fetch not in lands_to_remove :
lands_to_remove . extend ( fetch )
2024-12-20 07:10:53 -08:00
# Randomly choose fetches up to the desired number
2024-12-30 11:43:36 -08:00
while len ( chosen_fetches ) < desired_fetches + 3 and attempt_count < MAX_ATTEMPTS :
if not fetches : # If we run out of fetches to choose from
break
fetch_choice = random . choice ( fetches )
if use_scrython and self . set_max_card_price :
if self . price_check ( fetch_choice ) < = self . max_card_price * 1.1 :
2024-12-20 07:10:53 -08:00
chosen_fetches . append ( fetch_choice )
2024-12-21 00:58:12 -08:00
fetches . remove ( fetch_choice )
2024-12-30 11:43:36 -08:00
else :
chosen_fetches . append ( fetch_choice )
fetches . remove ( fetch_choice )
attempt_count + = 1
# Select final fetches to add
fetches_to_add = [ ]
available_fetches = chosen_fetches [ : desired_fetches ]
for fetch in available_fetches :
if fetch not in fetches_to_add :
fetches_to_add . append ( fetch )
if attempt_count > = MAX_ATTEMPTS :
logging . warning ( f " Reached maximum attempts ( { MAX_ATTEMPTS } ) while selecting fetch lands " )
2024-12-25 19:56:35 -08:00
for card in fetches_to_add :
2024-12-30 11:43:36 -08:00
self . add_card ( card , ' Land ' , None , 0 )
2024-12-21 00:58:12 -08:00
2024-12-27 12:02:50 -08:00
self . land_df = self . land_df [ ~ self . land_df [ ' name ' ] . isin ( lands_to_remove ) ]
self . land_df . to_csv ( f ' { csv_directory } /test_lands.csv ' , index = False )
2024-12-29 20:16:57 -08:00
2024-12-19 12:02:26 -08:00
def add_kindred_lands ( self ) :
2024-12-31 10:16:47 -08:00
""" Add lands that support tribal/kindred themes. """
logging . info ( ' Adding Kindred-themed lands ' )
# Standard Kindred support lands
KINDRED_STAPLES = [
{ ' name ' : ' Path of Ancestry ' , ' type ' : ' Land ' } ,
{ ' name ' : ' Three Tree City ' , ' type ' : ' Legendary Land ' } ,
{ ' name ' : ' Cavern of Souls ' , ' type ' : ' Land ' }
2024-12-30 11:43:36 -08:00
]
2024-12-31 10:16:47 -08:00
kindred_lands = KINDRED_STAPLES . copy ( )
lands_to_remove = set ( )
try :
# Process each Kindred theme
for theme in self . themes :
if ' Kindred ' in theme :
creature_type = theme . replace ( ' Kindred ' , ' ' )
logging . info ( f ' Searching for { creature_type } -specific lands ' )
# Filter lands by creature type
type_specific = self . land_df [
self . land_df [ ' text ' ] . notna ( ) &
( self . land_df [ ' text ' ] . str . contains ( creature_type , case = False ) |
self . land_df [ ' type ' ] . str . contains ( creature_type , case = False ) )
]
# Add matching lands to pool
for _ , row in type_specific . iterrows ( ) :
kindred_lands . append ( {
' name ' : row [ ' name ' ] ,
' type ' : row [ ' type ' ] ,
' manaCost ' : row [ ' manaCost ' ] ,
' manaValue ' : row [ ' manaValue ' ]
} )
lands_to_remove . add ( row [ ' name ' ] )
# Add lands to deck
for card in kindred_lands :
if card [ ' name ' ] not in self . card_library [ ' Card Name ' ] . values :
self . add_card ( card [ ' name ' ] , card [ ' type ' ] ,
None , 0 )
lands_to_remove . add ( card [ ' name ' ] )
# Update land database
self . land_df = self . land_df [ ~ self . land_df [ ' name ' ] . isin ( lands_to_remove ) ]
self . land_df . to_csv ( f ' { csv_directory } /test_lands.csv ' , index = False )
logging . info ( f ' Added { len ( lands_to_remove ) } Kindred-themed lands ' )
except Exception as e :
logging . error ( f " Error adding Kindred lands: { e } " )
raise
2024-12-20 07:10:53 -08:00
def add_dual_lands ( self ) :
# Determine dual-color lands available
2024-12-21 00:58:12 -08:00
# Determine if using the dual-type lands
2024-12-27 12:02:50 -08:00
print ( ' Would you like to include Dual-type lands (i.e. lands that count as both a Plains and a Swamp for example)? ' )
2024-12-21 00:58:12 -08:00
choice = self . questionnaire ( ' Confirm ' , True )
2024-12-27 12:02:50 -08:00
color_filter = [ ]
color_dict = {
' azorius ' : ' Plains Island ' ,
' dimir ' : ' Island Swamp ' ,
' rakdos ' : ' Swamp Mountain ' ,
' gruul ' : ' Mountain Forest ' ,
' selesnya ' : ' Forest Plains ' ,
' orzhov ' : ' Plains Swamp ' ,
' golgari ' : ' Swamp Forest ' ,
' simic ' : ' Forest Island ' ,
' izzet ' : ' Island Mountain ' ,
' boros ' : ' Mountain Plains '
}
2024-12-21 00:58:12 -08:00
2024-12-27 12:02:50 -08:00
if choice :
for key in color_dict :
if key in self . files_to_load :
color_filter . extend ( [ f ' Land — { color_dict [ key ] } ' , f ' Snow Land — { color_dict [ key ] } ' ] )
2024-12-20 13:39:23 -08:00
2024-12-27 12:02:50 -08:00
dual_df = self . land_df [ self . land_df [ ' type ' ] . isin ( color_filter ) ] . copy ( )
2024-12-20 13:39:23 -08:00
2024-12-27 12:02:50 -08:00
# Convert to list of card dictionaries
card_pool = [ ]
for _ , row in dual_df . iterrows ( ) :
card = {
' name ' : row [ ' name ' ] ,
' type ' : row [ ' type ' ] ,
' manaCost ' : row [ ' manaCost ' ] ,
' manaValue ' : row [ ' manaValue ' ]
}
card_pool . append ( card )
2024-12-20 13:39:23 -08:00
2024-12-27 12:02:50 -08:00
lands_to_remove = [ ]
for card in card_pool :
self . add_card ( card [ ' name ' ] , card [ ' type ' ] ,
card [ ' manaCost ' ] , card [ ' manaValue ' ] )
lands_to_remove . append ( card [ ' name ' ] )
self . land_df = self . land_df [ ~ self . land_df [ ' name ' ] . isin ( lands_to_remove ) ]
self . land_df . to_csv ( f ' { csv_directory } /test_lands.csv ' , index = False )
2024-12-20 13:39:23 -08:00
2024-12-31 09:18:02 -08:00
logging . info ( f ' Added { len ( card_pool ) } Dual-type land cards. ' )
2024-12-20 13:39:23 -08:00
2024-12-27 12:02:50 -08:00
if not choice :
2024-12-31 09:18:02 -08:00
logging . info ( ' Skipping adding Dual-type land cards. ' )
2024-12-27 12:02:50 -08:00
def add_triple_lands ( self ) :
# Determine if using Triome lands
print ( ' Would you like to include triome lands (i.e. lands that count as a Mountain, Forest, and Plains for example)? ' )
choice = self . questionnaire ( ' Confirm ' , True )
color_filter = [ ]
color_dict = {
' bant ' : ' Forest Plains Island ' ,
' esper ' : ' Plains Island Swamp ' ,
' grixis ' : ' Island Swamp Mountain ' ,
' jund ' : ' Swamp Mountain Forest ' ,
' naya ' : ' Mountain Forest Plains ' ,
' mardu ' : ' Mountain Plains Swamp ' ,
' abzan ' : ' Plains Swamp Forest ' ,
' sultai ' : ' Swamp Forest Island ' ,
' temur ' : ' Forest Island Mountain ' ,
' jeska ' : ' Island Mountain Plains '
}
if choice :
for key in color_dict :
if key in self . files_to_load :
color_filter . extend ( [ f ' Land — { color_dict [ key ] } ' ] )
2024-12-20 13:39:23 -08:00
2024-12-27 12:02:50 -08:00
triome_df = self . land_df [ self . land_df [ ' type ' ] . isin ( color_filter ) ] . copy ( )
# Convert to list of card dictionaries
card_pool = [ ]
for _ , row in triome_df . iterrows ( ) :
card = {
' name ' : row [ ' name ' ] ,
' type ' : row [ ' type ' ] ,
' manaCost ' : row [ ' manaCost ' ] ,
' manaValue ' : row [ ' manaValue ' ]
}
card_pool . append ( card )
2024-12-20 13:39:23 -08:00
2024-12-27 12:02:50 -08:00
lands_to_remove = [ ]
for card in card_pool :
self . add_card ( card [ ' name ' ] , card [ ' type ' ] ,
card [ ' manaCost ' ] , card [ ' manaValue ' ] )
lands_to_remove . append ( card [ ' name ' ] )
self . land_df = self . land_df [ ~ self . land_df [ ' name ' ] . isin ( lands_to_remove ) ]
self . land_df . to_csv ( f ' { csv_directory } /test_lands.csv ' , index = False )
2024-12-20 13:39:23 -08:00
2024-12-31 09:18:02 -08:00
logging . info ( f ' Added { len ( card_pool ) } Triome land cards. ' )
2024-12-26 17:20:58 -08:00
2024-12-27 12:02:50 -08:00
if not choice :
2024-12-31 09:18:02 -08:00
logging . info ( ' Skipping adding Triome land cards. ' )
2024-12-20 07:10:53 -08:00
def add_misc_lands ( self ) :
2024-12-31 10:16:47 -08:00
""" Add additional utility lands that fit the deck ' s color identity. """
logging . info ( ' Adding miscellaneous utility lands ' )
2024-12-20 13:39:23 -08:00
2024-12-31 10:16:47 -08:00
MIN_MISC_LANDS = 5
MAX_MISC_LANDS = 15
MAX_POOL_SIZE = 100
try :
# Create filtered pool of candidate lands
land_pool = ( self . land_df
. head ( MAX_POOL_SIZE )
. copy ( )
. reset_index ( drop = True ) )
# Convert to card dictionaries
card_pool = [
{
' name ' : row [ ' name ' ] ,
' type ' : row [ ' type ' ] ,
' manaCost ' : row [ ' manaCost ' ] ,
' manaValue ' : row [ ' manaValue ' ]
}
for _ , row in land_pool . iterrows ( )
if row [ ' name ' ] not in self . card_library [ ' Card Name ' ] . values
]
if not card_pool :
logging . warning ( " No eligible misc lands found " )
return
# Randomly select lands within constraints
target_count = random . randint ( MIN_MISC_LANDS , MAX_MISC_LANDS )
cards_to_add = [ ]
while card_pool and len ( cards_to_add ) < target_count :
card = random . choice ( card_pool )
card_pool . remove ( card )
# Check price if enabled
if use_scrython and self . set_max_card_price :
price = self . price_check ( card [ ' name ' ] )
if price > self . max_card_price * 1.1 :
continue
2024-12-21 00:58:12 -08:00
2024-12-31 10:16:47 -08:00
cards_to_add . append ( card )
# Add selected lands
lands_to_remove = set ( )
for card in cards_to_add :
self . add_card ( card [ ' name ' ] , card [ ' type ' ] ,
card [ ' manaCost ' ] , card [ ' manaValue ' ] )
lands_to_remove . add ( card [ ' name ' ] )
# Update land database
self . land_df = self . land_df [ ~ self . land_df [ ' name ' ] . isin ( lands_to_remove ) ]
self . land_df . to_csv ( f ' { csv_directory } /test_lands.csv ' , index = False )
logging . info ( f ' Added { len ( cards_to_add ) } miscellaneous lands ' )
except Exception as e :
logging . error ( f " Error adding misc lands: { e } " )
raise
2024-12-21 00:58:12 -08:00
def check_basics ( self ) :
2024-12-30 11:43:36 -08:00
""" Check and display counts of each basic land type. """
basic_lands = {
' Plains ' : 0 ,
' Island ' : 0 ,
' Swamp ' : 0 ,
' Mountain ' : 0 ,
' Forest ' : 0 ,
' Snow-Covered Plains ' : 0 ,
' Snow-Covered Island ' : 0 ,
' Snow-Covered Swamp ' : 0 ,
' Snow-Covered Mountain ' : 0 ,
' Snow-Covered Forest ' : 0
}
2024-12-21 00:58:12 -08:00
self . total_basics = 0
2024-12-30 11:43:36 -08:00
for land in basic_lands :
count = len ( self . card_library [ self . card_library [ ' Card Name ' ] == land ] )
basic_lands [ land ] = count
self . total_basics + = count
2024-12-31 09:18:02 -08:00
logging . info ( " \n Basic Land Counts: " )
2024-12-30 11:43:36 -08:00
for land , count in basic_lands . items ( ) :
if count > 0 :
2024-12-31 09:18:02 -08:00
logging . info ( f " { land } : { count } " )
logging . info ( f " Total basic lands: { self . total_basics } \n " )
2024-12-30 11:43:36 -08:00
2024-12-31 09:25:21 -08:00
def remove_basic ( self , max_attempts : int = 3 ) :
2024-12-27 12:02:50 -08:00
"""
Remove a basic land while maintaining color balance .
Attempts to remove from colors with more basics first .
2024-12-31 09:25:21 -08:00
Args :
max_attempts : Maximum number of removal attempts before falling back to non - basics
2024-12-27 12:02:50 -08:00
"""
logging . info ( ' Land count over ideal count, removing a basic land. ' )
2024-12-31 09:25:21 -08:00
2024-12-27 12:02:50 -08:00
color_to_basic = {
2024-12-31 09:25:21 -08:00
' W ' : ' Plains ' , ' U ' : ' Island ' , ' B ' : ' Swamp ' ,
' R ' : ' Mountain ' , ' G ' : ' Forest '
2024-12-27 12:02:50 -08:00
}
2024-12-31 09:25:21 -08:00
# Get current basic land counts using vectorized operations
basic_counts = {
basic : len ( self . card_library [ self . card_library [ ' Card Name ' ] == basic ] )
for color , basic in color_to_basic . items ( )
if color in self . colors
}
2024-12-29 20:16:57 -08:00
sum_basics = sum ( basic_counts . values ( ) )
2024-12-31 09:25:21 -08:00
attempts = 0
while attempts < max_attempts and sum_basics > self . min_basics :
if not basic_counts :
logging . warning ( " No basic lands found to remove " )
break
basic_land = max ( basic_counts . items ( ) , key = lambda x : x [ 1 ] ) [ 0 ]
2024-12-29 20:16:57 -08:00
try :
2024-12-31 09:25:21 -08:00
# Use boolean indexing for efficiency
mask = self . card_library [ ' Card Name ' ] == basic_land
if not mask . any ( ) :
basic_counts . pop ( basic_land )
continue
index_to_drop = self . card_library [ mask ] . index [ 0 ]
self . card_library = self . card_library . drop ( index_to_drop ) . reset_index ( drop = True )
2024-12-29 20:16:57 -08:00
logging . info ( f ' { basic_land } removed successfully ' )
2024-12-31 09:25:21 -08:00
return
2024-12-29 20:16:57 -08:00
except ( IndexError , KeyError ) as e :
logging . error ( f " Error removing { basic_land } : { e } " )
2024-12-31 09:25:21 -08:00
basic_counts . pop ( basic_land )
attempts + = 1
# If we couldn't remove a basic land, try removing a non-basic
logging . warning ( " Could not remove basic land, attempting to remove non-basic " )
self . remove_land ( )
2024-12-29 20:16:57 -08:00
2024-12-21 00:58:12 -08:00
def remove_land ( self ) :
2024-12-30 11:43:36 -08:00
""" Remove a random non-basic, non-staple land from the deck. """
2024-12-31 09:25:21 -08:00
logging . info ( ' Removing a random nonbasic land. ' )
2024-12-25 19:56:35 -08:00
2024-12-30 11:43:36 -08:00
# Define basic lands including snow-covered variants
basic_lands = [
' Plains ' , ' Island ' , ' Swamp ' , ' Mountain ' , ' Forest ' ,
' Snow-Covered Plains ' , ' Snow-Covered Island ' , ' Snow-Covered Swamp ' ,
' Snow-Covered Mountain ' , ' Snow-Covered Forest '
]
try :
# Filter for non-basic, non-staple lands
library_filter = self . card_library [
( self . card_library [ ' Card Type ' ] . str . contains ( ' Land ' ) ) &
( ~ self . card_library [ ' Card Name ' ] . isin ( basic_lands + self . staples ) )
] . copy ( )
if len ( library_filter ) == 0 :
2024-12-31 09:25:21 -08:00
logging . warning ( " No suitable non-basic lands found to remove. " )
2024-12-30 11:43:36 -08:00
return
# Select random land to remove
card_index = np . random . choice ( library_filter . index )
card_name = self . card_library . loc [ card_index , ' Card Name ' ]
2024-12-31 09:25:21 -08:00
logging . info ( f " Removing { card_name } " )
2024-12-30 11:43:36 -08:00
self . card_library . drop ( card_index , inplace = True )
self . card_library . reset_index ( drop = True , inplace = True )
2024-12-31 09:25:21 -08:00
logging . info ( " Card removed successfully. " )
2024-12-30 11:43:36 -08:00
except Exception as e :
logging . error ( f " Error removing land: { e } " )
2024-12-31 09:25:21 -08:00
logging . warning ( " Failed to remove land card. " )
2024-12-30 11:43:36 -08:00
2024-12-29 20:16:57 -08:00
def count_pips ( self ) :
2024-12-31 10:16:47 -08:00
""" Count and display the number of colored mana symbols in casting costs using vectorized operations. """
2024-12-31 09:25:21 -08:00
logging . info ( ' Analyzing color pip distribution... ' )
2024-12-29 20:16:57 -08:00
2024-12-31 10:16:47 -08:00
# Define colors to check
colors = [ ' W ' , ' U ' , ' B ' , ' R ' , ' G ' ]
2024-12-30 11:43:36 -08:00
2024-12-31 10:16:47 -08:00
# Use vectorized string operations
mana_costs = self . card_library [ ' Mana Cost ' ] . dropna ( )
pip_counts = { color : mana_costs . str . count ( color ) . sum ( ) for color in colors }
2024-12-30 11:43:36 -08:00
total_pips = sum ( pip_counts . values ( ) )
if total_pips == 0 :
2024-12-31 09:25:21 -08:00
logging . error ( " No colored mana symbols found in casting costs. " )
2024-12-30 11:43:36 -08:00
return
2024-12-31 09:25:21 -08:00
logging . info ( " \n Color Pip Distribution: " )
2024-12-30 11:43:36 -08:00
for color , count in pip_counts . items ( ) :
if count > 0 :
percentage = ( count / total_pips ) * 100
print ( f " { color } : { count } pips ( { percentage : .1f } %) " )
2024-12-31 09:25:21 -08:00
logging . info ( f " Total colored pips: { total_pips } \n " )
2024-12-29 20:16:57 -08:00
def get_cmc ( self ) :
2024-12-30 11:43:36 -08:00
""" Calculate average converted mana cost of non-land cards. """
logging . info ( ' Calculating average mana value of non-land cards. ' )
try :
# Filter non-land cards
non_land = self . card_library [
~ self . card_library [ ' Card Type ' ] . str . contains ( ' Land ' )
] . copy ( )
if non_land . empty :
logging . warning ( " No non-land cards found " )
self . cmc = 0.0
else :
total_cmc = non_land [ ' Mana Value ' ] . sum ( )
self . cmc = round ( total_cmc / len ( non_land ) , 2 )
self . commander_dict . update ( { ' CMC ' : float ( self . cmc ) } )
logging . info ( f " Average CMC: { self . cmc } " )
except Exception as e :
logging . error ( f " Error calculating CMC: { e } " )
self . cmc = 0.0
2024-12-31 09:18:02 -08:00
def weight_by_theme ( self , tag , ideal = 1 , weight = 1 , df = None ) :
2024-12-25 19:56:35 -08:00
# First grab the first 50/30/20 cards that match each theme
2024-12-26 17:20:58 -08:00
""" Add cards with specific tag up to ideal_value count """
2024-12-29 20:16:57 -08:00
ideal_value = math . ceil ( ideal * weight * 0.9 )
2024-12-31 09:25:21 -08:00
print ( f ' Finding { ideal_value } cards with the " { tag } " tag... ' )
2024-12-26 17:20:58 -08:00
if ' Kindred ' in tag :
2024-12-30 12:06:46 -08:00
tags = [ tag , ' Kindred Support ' ]
2024-12-26 17:20:58 -08:00
else :
tags = [ tag ]
# Filter cards with the given tag
2024-12-31 09:18:02 -08:00
tag_df = df . copy ( )
2024-12-26 17:20:58 -08:00
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
2024-12-27 12:02:50 -08:00
pool_size = int ( ideal_value * random . randint ( 15 , 20 ) / 10 )
2024-12-26 17:20:58 -08:00
tag_df = tag_df . head ( pool_size )
2024-12-25 19:56:35 -08:00
2024-12-26 17:20:58 -08:00
# Convert to list of card dictionaries
2024-12-30 11:43:36 -08:00
card_pool = [
{
2024-12-26 17:20:58 -08:00
' name ' : row [ ' name ' ] ,
' type ' : row [ ' type ' ] ,
' manaCost ' : row [ ' manaCost ' ] ,
' manaValue ' : row [ ' manaValue ' ]
}
2024-12-30 11:43:36 -08:00
for _ , row in tag_df . iterrows ( )
]
2024-12-26 17:20:58 -08:00
# 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 )
2024-12-25 19:56:35 -08:00
2024-12-26 17:20:58 -08:00
# Check price constraints if enabled
if use_scrython and self . set_max_card_price :
price = self . price_check ( card [ ' name ' ] )
if price > self . max_card_price * 1.1 :
continue
2024-12-25 19:56:35 -08:00
2024-12-26 17:20:58 -08:00
# Add card if not already in library
2024-12-27 12:02:50 -08:00
if card [ ' name ' ] in multiple_copy_cards :
if card [ ' name ' ] == ' Nazgûl ' :
for _ in range ( 9 ) :
cards_to_add . append ( card )
elif card [ ' name ' ] == ' Seven Dwarves ' :
for _ in range ( 7 ) :
cards_to_add . append ( card )
else :
num_to_add = ideal_value - len ( cards_to_add )
2024-12-30 12:06:46 -08:00
for _ in range ( num_to_add ) :
2024-12-27 12:02:50 -08:00
cards_to_add . append ( card )
elif ( card [ ' name ' ] not in multiple_copy_cards
and card [ ' name ' ] not in self . card_library [ ' Card Name ' ] . values ) :
2024-12-26 17:20:58 -08:00
cards_to_add . append ( card )
2024-12-27 12:02:50 -08:00
elif ( card [ ' name ' ] not in multiple_copy_cards
and card [ ' name ' ] in self . card_library [ ' Card Name ' ] . values ) :
2024-12-31 09:25:21 -08:00
logging . warning ( f " { card [ ' name ' ] } already in Library, skipping it. " )
2024-12-27 12:02:50 -08:00
continue
2024-12-26 17:20:58 -08:00
# Add selected cards to library
for card in cards_to_add :
self . add_card ( card [ ' name ' ] , card [ ' type ' ] ,
card [ ' manaCost ' ] , card [ ' manaValue ' ] )
2024-12-29 20:16:57 -08:00
card_pool_names = [ item [ ' name ' ] for item in card_pool ]
self . full_df = self . full_df [ ~ self . full_df [ ' name ' ] . isin ( card_pool_names ) ]
self . noncreature_df = self . noncreature_df [ ~ self . noncreature_df [ ' name ' ] . isin ( card_pool_names ) ]
2024-12-31 09:18:02 -08:00
logging . info ( f ' Added { len ( cards_to_add ) } { tag } cards ' )
2024-12-27 12:02:50 -08:00
#tag_df.to_csv(f'{csv_directory}/test_{tag}.csv', index=False)
2024-12-25 19:56:35 -08:00
2024-12-31 09:18:02 -08:00
def add_by_tags ( self , tag , ideal_value = 1 , df = None ) :
2024-12-26 11:59:30 -08:00
""" Add cards with specific tag up to ideal_value count """
2024-12-31 09:25:21 -08:00
print ( f ' Finding { ideal_value } cards with the " { tag } " tag... ' )
2024-12-30 11:43:36 -08:00
2024-12-26 11:59:30 -08:00
# Filter cards with the given tag
2024-12-29 20:16:57 -08:00
skip_creatures = self . creature_cards > self . ideal_creature_count * 1.1
2024-12-31 09:18:02 -08:00
tag_df = df . copy ( )
2024-12-26 11:59:30 -08:00
tag_df . sort_values ( by = ' edhrecRank ' , inplace = True )
tag_df = tag_df [ tag_df [ ' themeTags ' ] . apply ( lambda x : tag in x ) ]
# Take top cards based on ideal value
pool_size = int ( ideal_value * random . randint ( 2 , 3 ) )
tag_df = tag_df . head ( pool_size )
2024-12-30 11:43:36 -08:00
2024-12-26 11:59:30 -08:00
# Convert to list of card dictionaries
2024-12-30 11:43:36 -08:00
card_pool = [
{
2024-12-26 11:59:30 -08:00
' name ' : row [ ' name ' ] ,
' type ' : row [ ' type ' ] ,
' manaCost ' : row [ ' manaCost ' ] ,
' manaValue ' : row [ ' manaValue ' ]
}
2024-12-30 11:43:36 -08:00
for _ , row in tag_df . iterrows ( )
]
2024-12-26 11:59:30 -08:00
# 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 )
2024-12-30 11:43:36 -08:00
2024-12-26 11:59:30 -08:00
# Check price constraints if enabled
if use_scrython and self . set_max_card_price :
price = self . price_check ( card [ ' name ' ] )
if price > self . max_card_price * 1.1 :
2024-12-25 19:56:35 -08:00
continue
2024-12-30 11:43:36 -08:00
2024-12-26 11:59:30 -08:00
# Add card if not already in library
if card [ ' name ' ] not in self . card_library [ ' Card Name ' ] . values :
2024-12-29 20:16:57 -08:00
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 )
2024-12-30 11:43:36 -08:00
2024-12-26 11:59:30 -08:00
# Add selected cards to library
for card in cards_to_add :
2024-12-29 20:16:57 -08:00
if len ( self . card_library ) < 100 :
self . add_card ( card [ ' name ' ] , card [ ' type ' ] ,
card [ ' manaCost ' ] , card [ ' manaValue ' ] )
else :
continue
2024-12-30 11:43:36 -08:00
2024-12-29 20:16:57 -08:00
card_pool_names = [ item [ ' name ' ] for item in card_pool ]
self . full_df = self . full_df [ ~ self . full_df [ ' name ' ] . isin ( card_pool_names ) ]
self . noncreature_df = self . noncreature_df [ ~ self . noncreature_df [ ' name ' ] . isin ( card_pool_names ) ]
2024-12-31 09:18:02 -08:00
logging . info ( f ' Added { len ( cards_to_add ) } { tag } cards ' )
2024-12-27 12:02:50 -08:00
#tag_df.to_csv(f'{csv_directory}/test_{tag}.csv', index=False)
2024-12-06 12:04:39 -08:00
def add_creatures ( self ) :
2024-12-27 12:02:50 -08:00
"""
Add creatures to the deck based on themes and weights .
2024-12-25 19:56:35 -08:00
2024-12-27 12:02:50 -08:00
This method processes the primary , secondary , and tertiary themes to add
creatures proportionally according to their weights . The total number of
creatures added will approximate the ideal_creature_count .
Themes are processed in order of importance ( primary - > secondary - > tertiary )
with error handling to ensure the deck building process continues even if
a particular theme encounters issues .
"""
2024-12-31 09:25:21 -08:00
print ( f ' Adding creatures to deck based on the ideal creature count of { self . ideal_creature_count } ... ' )
2024-12-27 12:02:50 -08:00
try :
2024-12-30 11:43:36 -08:00
if self . hidden_theme :
2024-12-31 09:25:21 -08:00
print ( f ' Processing Hidden theme: { self . hidden_theme } ' )
2024-12-31 09:18:02 -08:00
self . weight_by_theme ( self . hidden_theme , self . ideal_creature_count , self . hidden_weight , self . creature_df )
2024-12-31 09:25:21 -08:00
print ( f ' Processing primary theme: { self . primary_theme } ' )
2024-12-31 09:18:02 -08:00
self . weight_by_theme ( self . primary_theme , self . ideal_creature_count , self . primary_weight , self . creature_df )
2024-12-27 12:02:50 -08:00
if self . secondary_theme :
2024-12-31 09:25:21 -08:00
print ( f ' Processing secondary theme: { self . secondary_theme } ' )
2024-12-31 09:18:02 -08:00
self . weight_by_theme ( self . secondary_theme , self . ideal_creature_count , self . secondary_weight , self . creature_df )
2024-12-27 12:02:50 -08:00
if self . tertiary_theme :
2024-12-31 09:25:21 -08:00
print ( f ' Processing tertiary theme: { self . tertiary_theme } ' )
2024-12-31 09:18:02 -08:00
self . weight_by_theme ( self . tertiary_theme , self . ideal_creature_count , self . tertiary_weight , self . creature_df )
2024-12-27 12:02:50 -08:00
except Exception as e :
logging . error ( f " Error while adding creatures: { e } " )
finally :
self . organize_library ( )
2024-12-31 09:18:02 -08:00
logging . info ( f ' Creature addition complete. Total creatures (including commander): { self . creature_cards } ' )
2024-12-29 20:16:57 -08:00
2024-12-25 19:56:35 -08:00
def add_ramp ( self ) :
2024-12-31 09:18:02 -08:00
try :
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 ( ' Ramp ' , math . ceil ( self . ideal_ramp / 2 ) , self . noncreature_df )
except Exception as e :
logging . error ( f " Error while adding Ramp: { e } " )
finally :
logging . info ( ' Adding Ramp complete. ' )
2024-12-25 19:56:35 -08:00
def add_interaction ( self ) :
2024-12-31 09:18:02 -08:00
try :
self . add_by_tags ( ' Removal ' , self . ideal_removal , self . noncreature_nonplaneswaker_df )
self . add_by_tags ( ' Protection ' , self . ideal_protection , self . noncreature_nonplaneswaker_df )
except Exception as e :
logging . error ( f " Error while adding Interaction: { e } " )
finally :
logging . info ( ' Adding Interaction complete. ' )
2024-12-25 19:56:35 -08:00
def add_board_wipes ( self ) :
2024-12-31 09:18:02 -08:00
try :
self . add_by_tags ( ' Board Wipes ' , self . ideal_wipes , self . full_df )
except Exception as e :
logging . error ( f " Error while adding Board Wipes: { e } " )
finally :
logging . info ( ' Adding Board Wipes complete. ' )
2024-12-25 19:56:35 -08:00
def add_card_advantage ( self ) :
2024-12-31 09:18:02 -08:00
try :
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 )
except Exception as e :
logging . error ( f " Error while adding Card Draw: { e } " )
finally :
logging . info ( ' Adding Card Draw complete. ' )
2024-12-29 20:16:57 -08:00
def fill_out_deck ( self ) :
2024-12-30 11:43:36 -08:00
""" Fill out the deck to 100 cards with theme-appropriate cards. """
2024-12-31 09:18:02 -08:00
logging . info ( ' Filling out the Library to 100 with cards fitting the themes. ' )
2024-12-30 11:43:36 -08:00
cards_needed = 100 - len ( self . card_library )
if cards_needed < = 0 :
return
logging . info ( f " Need to add { cards_needed } more cards " )
2024-12-31 09:18:02 -08:00
# Define maximum attempts and timeout
MAX_ATTEMPTS = max ( 20 , cards_needed * 2 )
MAX_TIME = 60 # Maximum time in seconds
start_time = time . time ( )
2024-12-30 11:43:36 -08:00
attempts = 0
while len ( self . card_library ) < 100 and attempts < MAX_ATTEMPTS :
2024-12-31 09:18:02 -08:00
# Check timeout
if time . time ( ) - start_time > MAX_TIME :
logging . error ( " Timeout reached while filling deck " )
break
2024-12-30 11:43:36 -08:00
initial_count = len ( self . card_library )
remaining = 100 - len ( self . card_library )
# Adjust weights based on remaining cards needed
weight_multiplier = remaining / cards_needed
2024-12-31 09:18:02 -08:00
try :
# Add cards from each theme with adjusted weights
if self . tertiary_theme :
self . add_by_tags ( self . tertiary_theme ,
math . ceil ( self . tertiary_weight * 10 * weight_multiplier ) ,
self . noncreature_df )
if self . secondary_theme :
self . add_by_tags ( self . secondary_theme ,
math . ceil ( self . secondary_weight * 3 * weight_multiplier ) ,
self . noncreature_df )
self . add_by_tags ( self . primary_theme ,
math . ceil ( self . primary_weight * 2 * weight_multiplier ) ,
self . noncreature_df )
# Check if we made progress
if len ( self . card_library ) == initial_count :
attempts + = 1
if attempts % 5 == 0 :
logging . warning ( f " Made { attempts } attempts, still need { 100 - len ( self . card_library ) } cards " )
# Break early if we're stuck
if attempts > = MAX_ATTEMPTS / 2 and len ( self . card_library ) < initial_count + ( cards_needed / 4 ) :
logging . warning ( " Insufficient progress being made, breaking early " )
break
except Exception as e :
logging . error ( f " Error while adding cards: { e } " )
2024-12-30 11:43:36 -08:00
attempts + = 1
2024-12-31 09:18:02 -08:00
2024-12-30 11:43:36 -08:00
final_count = len ( self . card_library )
if final_count < 100 :
2024-12-31 09:18:02 -08:00
message = f " \n WARNING: Deck is incomplete with { final_count } cards. Manual additions may be needed. "
logging . warning ( message )
2024-12-30 11:43:36 -08:00
else :
logging . info ( f " Successfully filled deck to { final_count } cards in { attempts } attempts " )
def main ( ) :
""" Main entry point for deck builder application. """
build_deck = DeckBuilder ( )
build_deck . determine_commander ( )
pprint . pprint ( build_deck . commander_dict , sort_dicts = False )
if __name__ == ' __main__ ' :
main ( )