Refined chunks of logic using Traycer for analysis

This commit is contained in:
mwisnowski 2024-12-31 10:16:47 -08:00
parent a5f6e4f09e
commit 5a92c04810

View file

@ -344,152 +344,132 @@ class DeckBuilder:
self.card_library.to_csv(f'{csv_directory}/test_deck_done.csv', index=False) 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) self.full_df.to_csv(f'{csv_directory}/test_all_after_done.csv', index=False)
def determine_color_identity(self): def determine_color_identity(self) -> None:
# Determine the color identity for later """Determine the deck's color identity and set related attributes."""
# Mono color # Single color mapping
if self.color_identity == 'COLORLESS': mono_color_map = {
self.color_identity_full = 'Colorless' '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',
'bant', 'jeska', 'glint', 'dune','witch', 'yore', 'ink', 'wubrg'])
}
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'
self.files_to_load = ['colorless'] self.files_to_load = ['colorless']
elif self.color_identity == 'B':
self.color_identity_full = 'Black' except Exception as e:
self.files_to_load = ['colorless', 'black'] logging.error(f"Error in determine_color_identity: {e}")
elif self.color_identity == 'G': raise
self.color_identity_full = 'Green'
self.files_to_load = ['colorless', 'green']
elif self.color_identity == 'R':
self.color_identity_full = 'Red'
self.files_to_load = ['colorless', 'red']
elif self.color_identity == 'U':
self.color_identity_full = 'Blue'
self.files_to_load = ['colorless', 'blue']
elif self.color_identity == 'W':
self.color_identity_full = 'White'
self.files_to_load = ['colorless', 'white']
# Two-color
elif self.color_identity == 'B, G':
self.color_identity_full = 'Golgari: Black/Green'
self.color_identity_options = ['B', 'G', 'B, G']
self.files_to_load = ['colorless', 'black', 'green', 'golgari']
elif self.color_identity == 'B, R':
self.color_identity_full = 'Rakdos: Black/Red'
self.color_identity_options = ['B', 'R', 'B, R']
self.files_to_load = ['colorless', 'black', 'red', 'rakdos']
elif self.color_identity == 'B, U':
self.color_identity_full = 'Dimir: Black/Blue'
self.color_identity_options = ['B', 'U', 'B, U']
self.files_to_load = ['colorless', 'black', 'blue', 'dimir']
elif self.color_identity == 'B, W':
self.color_identity_full = 'Orzhov: Black/White'
self.color_identity_options = ['B', 'W', 'B, W']
self.files_to_load = ['colorless', 'black', 'white', 'orzhov']
elif self.color_identity == 'G, R':
self.color_identity_full = 'Gruul: Green/Red'
self.color_identity_options = ['G', 'R', 'G, R']
self.files_to_load = ['colorless', 'green', 'red', 'gruul']
elif self.color_identity == 'G, U':
self.color_identity_full = 'Simic: Green/Blue'
self.color_identity_options = ['G', 'U', 'G, U']
self.files_to_load = ['colorless', 'green', 'blue', 'simic']
elif self.color_identity == 'G, W':
self.color_identity_full = 'Selesnya: Green/White'
self.color_identity_options = ['G', 'W', 'G, W']
self.files_to_load = ['colorless', 'green', 'white', 'selesnya']
elif self.color_identity == 'R, U':
self.color_identity_full = 'Izzet Blue/Red'
self.color_identity_options = ['U', 'R', 'U, R']
self.files_to_load = ['colorless', 'blue', 'red', 'azorius']
elif self.color_identity == 'U, W':
self.color_identity_full = 'Azorius: Blue/White'
self.color_identity_options = ['U', 'W', 'U, W']
self.files_to_load = ['colorless', 'blue', 'white', 'azorius']
elif self.color_identity == 'R, W':
self.color_identity_full = 'Boros: Red/White'
self.color_identity_options = ['R', 'W', 'R, W']
self.files_to_load = ['colorless', 'red', 'white', 'boros']
# Tri-color
elif self.color_identity == 'B, G, U':
self.color_identity_full = 'Sultai: Black/Blue/Green'
self.color_identity_options = ['B', 'G', 'U', 'B, G', 'B, U', 'G, U', 'B, G, U']
self.files_to_load = ['colorless', 'black', 'blue', 'green', 'dimir', 'golgari', 'simic', 'sultai']
elif self.color_identity == 'B, G, R':
self.color_identity_full = 'Jund: Black/Green/Red'
self.color_identity_options = ['B', 'G', 'R', 'B, G', 'B, R', 'G, R', 'B, G, R']
self.files_to_load = ['colorless', 'black', 'green', 'red', 'golgari', 'rakdos', 'gruul', 'jund']
elif self.color_identity == 'B, G, W':
self.color_identity_full = 'Abzan: Black/Green/White'
self.color_identity_options = ['B', 'G', 'W', 'B, G', 'B, W', 'G, W', 'B, G, W']
self.files_to_load = ['colorless', 'black', 'green', 'white', 'golgari', 'orzhov', 'selesnya', 'abzan']
elif self.color_identity == 'B, R, U':
self.color_identity_full = 'Grixis: Black/Blue/Red'
self.color_identity_options = ['B', 'R', 'U', 'B, R', 'B, U', 'R, U', 'B, R, U']
self.files_to_load = ['colorless', 'black', 'blue', 'red', 'dimir', 'rakdos', 'izzet', 'grixis']
elif self.color_identity == 'B, R, W':
self.color_identity_full = 'Mardu: Black/Red/White'
self.color_identity_options = ['B', 'R', 'W', 'B, R', 'B, W', 'R, W', 'B, R, W']
self.files_to_load = ['colorless', 'black', 'red', 'white', 'rakdos', 'orzhov', 'boros', 'mardu']
elif self.color_identity == 'B, U, W':
self.color_identity_full = 'Esper: Black/Blue/White'
self.color_identity_options = ['B', 'U', 'W', 'B, U', 'B, W', 'U, W', 'B, U, W']
self.files_to_load = ['colorless', 'black', 'blue', 'white', 'dimir', 'orzhov', 'azorius', 'esper']
elif self.color_identity == 'G, R, U':
self.color_identity_full = 'Temur: Blue/Green/Red'
self.color_identity_options = ['G', 'R', 'U', 'G, R', 'G, U', 'R, U', 'G, R, U']
self.files_to_load = ['colorless', 'green', 'red', 'blue', 'simic', 'izzet', 'gruul', 'temur']
elif self.color_identity == 'G, R, W':
self.color_identity_full = 'Naya: Green/Red/White'
self.color_identity_options = ['G', 'R', 'W', 'G, R', 'G, W', 'R, W', 'G, R, W']
self.files_to_load = ['colorless', 'green', 'red', 'white', 'gruul', 'selesnya', 'boros', 'naya']
elif self.color_identity == 'G, U, W':
self.color_identity_full = 'Bant: Blue/Green/White'
self.color_identity_options = ['G', 'U', 'W', 'G, U', 'G, W', 'U, W', 'G, U, W']
self.files_to_load = ['colorless', 'green', 'blue', 'white', 'simic', 'azorius', 'selesnya', 'bant']
elif self.color_identity == 'U, R, W':
self.color_identity_full = 'Jeskai: Blue/Red/White'
self.color_identity_options = ['U', 'R', 'W', 'U, R', 'U, W', 'R, W', 'U, R, W']
self.files_to_load = ['colorless', 'blue', 'red', 'white', 'izzet', 'azorius', 'boros', 'jeskai']
# Quad-color
elif self.color_identity == 'B, G, R, U':
self.color_identity_full = 'Glint: Black/Blue/Green/Red'
self.color_identity_options = ['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']
self.files_to_load = ['colorless', 'black', 'blue', 'green', 'red', 'golgari', 'rakdos', 'dimir', 'gruul',
'simic', 'izzet', 'jund', 'sultai', 'grixis', 'temur', 'glint']
elif self.color_identity == 'B, G, R, W':
self.color_identity_full = 'Dune: Black/Green/Red/White'
self.color_identity_options = ['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']
self.files_to_load = ['colorless', 'black', 'green', 'red', 'white', 'golgari', 'rakdos', 'orzhov', 'gruul',
'selesnya', 'boros', 'jund', 'abzan', 'mardu', 'naya', 'dune']
elif self.color_identity == 'B, G, U, W':
self.color_identity_full = 'Witch: Black/Blue/Green/White'
self.color_identity_options = ['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']
self.files_to_load = ['colorless', 'black', 'blue', 'green', 'white', 'golgari', 'dimir', 'orzhov', 'simic',
'selesnya', 'azorius', 'sultai', 'abzan', 'esper', 'bant', 'glint']
elif self.color_identity == 'B, R, U, W':
self.color_identity_full = 'Yore: Black/Blue/Red/White'
self.color_identity_options = ['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']
self.files_to_load = ['colorless', 'black', 'blue', 'red', 'white', 'rakdos', 'dimir', 'orzhov', 'izzet',
'boros', 'azorius', 'grixis', 'mardu', 'esper', 'mardu', 'glint']
elif self.color_identity == 'G, R, U, W':
self.color_identity_full = 'Ink: Blue/Green/Red/White'
self.color_identity_options = ['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']
self.files_to_load = ['colorless', 'blue', 'green', 'red', 'white', 'gruul', 'simic', 'selesnya', 'izzet',
'boros', 'azorius', 'temur', 'naya', 'bant', 'jeskai', 'glint']
elif self.color_identity == 'B, G, R, U, W':
self.color_identity_full = 'WUBRG: All colors'
self.color_identity_options = ['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']
self.files_to_load = ['colorless', 'black', 'green', 'red', 'blue', 'white', 'golgari', 'rakdos',' dimir',
'orzhov', 'gruul', 'simic', 'selesnya', 'izzet', 'boros', 'azorius', 'jund', 'sultai', 'abzan',
'grixis', 'mardu', 'esper', 'temur', 'naya', 'bant', 'jeska', 'glint', 'dune','witch', 'yore',
'ink']
def setup_dataframes(self): def setup_dataframes(self):
all_df = [] all_df = []
@ -561,6 +541,7 @@ class DeckBuilder:
weights = weights_default.copy() weights = weights_default.copy()
themes.remove(choice) themes.remove(choice)
themes.append('Stop Here') themes.append('Stop Here')
self.primary_weight = weights['primary']
secondary_theme_chosen = False secondary_theme_chosen = False
tertiary_theme_chosen = False tertiary_theme_chosen = False
@ -592,14 +573,14 @@ class DeckBuilder:
secondary_theme_chosen = True secondary_theme_chosen = True
# Set weights for primary/secondary themes # Set weights for primary/secondary themes
if 'Kindred' in self.primary_theme and 'Kindred' not in self.secondary_theme: if 'Kindred' in self.primary_theme and 'Kindred' not in self.secondary_theme:
weights['primary'] -= 0.15 # 0.85 weights['primary'] -= 0.1 # 0.8
weights['secondary'] += 0.15 # 0.15 weights['secondary'] += 0.1 # 0.1
elif 'Kindred' in self.primary_theme and 'Kindred' in self.secondary_theme: elif 'Kindred' in self.primary_theme and 'Kindred' in self.secondary_theme:
weights['primary'] -= 0.7 # 0.7
weights['secondary'] += 0.3 # 0.3
else:
weights['primary'] -= 0.4 # 0.6 weights['primary'] -= 0.4 # 0.6
weights['secondary'] += 0.4 # 0.4 weights['secondary'] += 0.4 # 0.4
else:
weights['primary'] -= 0.3 # 0.7
weights['secondary'] += 0.3 # 0.3
self.primary_weight = weights['primary'] self.primary_weight = weights['primary']
self.secondary_weight = weights['secondary'] self.secondary_weight = weights['secondary']
break break
@ -632,8 +613,8 @@ class DeckBuilder:
weights['secondary'] += 0.1 # 0.1 weights['secondary'] += 0.1 # 0.1
weights['tertiary'] += 0.1 # 0.1 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: elif 'Kindred' in self.primary_theme and 'Kindred' in self.secondary_theme and 'Kindred' not in self.tertiary_theme:
weights['primary'] -= 0.4 # 0.6 weights['primary'] -= 0.3 # 0.7
weights['secondary'] += 0.3 # 0.3 weights['secondary'] += 0.2 # 0.2
weights['tertiary'] += 0.1 # 0.1 weights['tertiary'] += 0.1 # 0.1
elif 'Kindred' in self.primary_theme and 'Kindred' in self.secondary_theme and 'Kindred' in self.tertiary_theme: elif 'Kindred' in self.primary_theme and 'Kindred' in self.secondary_theme and 'Kindred' in self.tertiary_theme:
weights['primary'] -= 0.5 # 0.5 weights['primary'] -= 0.5 # 0.5
@ -854,17 +835,17 @@ class DeckBuilder:
def add_card(self, card: str, card_type: str, mana_cost: str, mana_value: int, is_commander: bool = False) -> None: def add_card(self, card: str, card_type: str, mana_cost: str, mana_value: int, is_commander: bool = False) -> None:
"""Add a card to the deck library with price checking if enabled. """Add a card to the deck library with price checking if enabled.
Args: Args:
card (str): Name of the card to add card (str): Name of the card to add
card_type (str): Type of the card (e.g., 'Creature', 'Instant') card_type (str): Type of the card (e.g., 'Creature', 'Instant')
mana_cost (str): Mana cost string representation mana_cost (str): Mana cost string representation
mana_value (int): Converted mana cost/mana value mana_value (int): Converted mana cost/mana value
is_commander (bool, optional): Whether this card is the commander. Defaults to False. is_commander (bool, optional): Whether this card is the commander. Defaults to False.
Returns: Returns:
None None
Raises: Raises:
ValueError: If card price exceeds maximum allowed price when price checking is enabled ValueError: If card price exceeds maximum allowed price when price checking is enabled
""" """
@ -877,10 +858,14 @@ class DeckBuilder:
# Handle price checking # Handle price checking
card_price = 0.0 card_price = 0.0
if use_scrython and self.set_max_card_price: if use_scrython and self.set_max_card_price:
card_price = self.card_prices.get(card) or self.price_check(card) or 0.0 # 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)
# Skip if card is too expensive # Skip if card is too expensive
if card_price > self.max_card_price * 1.1: if card_price is not None and card_price > self.max_card_price * 1.1:
logging.info(f"Skipping {card} - price {card_price} exceeds maximum") logging.info(f"Skipping {card} - price {card_price} exceeds maximum")
return return
@ -941,26 +926,22 @@ class DeckBuilder:
.reset_index(drop=True) .reset_index(drop=True)
) )
def commander_to_top(self): def commander_to_top(self) -> None:
"""Move commander card to the top of the library.""" """Move commander card to the top of the library while preserving commander status."""
try: try:
# Extract commander row
commander_row = self.card_library[self.card_library['Commander']].copy() commander_row = self.card_library[self.card_library['Commander']].copy()
if commander_row.empty: if commander_row.empty:
logging.warning("No commander found in library") logging.warning("No commander found in library")
return return
# Remove commander from main library
self.card_library = self.card_library[~self.card_library['Commander']] self.card_library = self.card_library[~self.card_library['Commander']]
# Concatenate with commander at top
self.card_library = pd.concat([commander_row, self.card_library], ignore_index=True) self.card_library = pd.concat([commander_row, self.card_library], ignore_index=True)
self.card_library = self.card_library.drop(columns=['Commander'])
logging.info(f"Successfully moved commander '{commander_row['Card Name'].iloc[0]}' to top") commander_name = commander_row['Card Name'].iloc[0]
logging.info(f"Successfully moved commander '{commander_name}' to top")
except Exception as e: except Exception as e:
logging.error(f"Error moving commander to top: {e}") logging.error(f"Error moving commander to top: {str(e)}")
def concatenate_duplicates(self): def concatenate_duplicates(self):
"""Handle duplicate cards in the library while maintaining data integrity.""" """Handle duplicate cards in the library while maintaining data integrity."""
duplicate_lists = basic_lands + multiple_copy_cards duplicate_lists = basic_lands + multiple_copy_cards
@ -1008,44 +989,62 @@ class DeckBuilder:
logging.warning(f"Attempted to drop non-existent index {index}") logging.warning(f"Attempted to drop non-existent index {index}")
def add_lands(self): def add_lands(self):
""" """
Begin the process to add lands, the number will depend on ideal land count, ramp, Add lands to the deck based on ideal count and deck requirements.
and if any utility lands may be helpful.
By default, ({self.ideal_land_count} - 5) basic lands will be added, distributed The process follows these steps:
across the commander color identity. These will be removed for utility lands, 1. Add basic lands distributed by color identity
multi-color producing lands, fetches, and any MDFCs added later. 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
""" """
MAX_ADJUSTMENT_ATTEMPTS = 10
self.total_basics = 0 self.total_basics = 0
self.add_basics()
self.check_basics()
self.add_standard_non_basics()
self.add_fetches()
if 'Kindred' in ' '.join(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() try:
# Add lands in sequence
for index, row in self.land_df.iterrows(): self.add_basics()
for land in self.card_library: self.check_basics()
if land in row['name']: self.add_standard_non_basics()
self.drop_card(self.land_df, index) self.add_fetches()
self.land_df.to_csv(f'{csv_directory}/test_lands.csv', index=False) # Add theme and color-specific lands
if any('Kindred' in theme for theme in self.themes):
# If over ideal land count, remove random basics until ideal land count self.add_kindred_lands()
self.check_basics() if len(self.colors) >= 2:
logging.info('Checking total land count to ensure it\'s within ideal count.\n\n') self.add_dual_lands()
self.organize_library() if len(self.colors) >= 3:
while self.land_cards > int(self.ideal_land_count): self.add_triple_lands()
logging.info(f'Total lands: {self.land_cards}')
logging.info(f'Ideal lands: {self.ideal_land_count}') self.add_misc_lands()
self.remove_basic()
# 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...')
self.organize_library() self.organize_library()
logging.info(f'Total lands: {self.land_cards}') 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
def add_basics(self): def add_basics(self):
base_basics = self.ideal_land_count - 10 # Reserve 10 slots for non-basic lands base_basics = self.ideal_land_count - 10 # Reserve 10 slots for non-basic lands
@ -1097,34 +1096,38 @@ class DeckBuilder:
self.land_df.to_csv(f'{csv_directory}/test_lands.csv', index=False) self.land_df.to_csv(f'{csv_directory}/test_lands.csv', index=False)
def add_standard_non_basics(self): def add_standard_non_basics(self):
# Add lands that are good in most any commander deck """Add staple utility lands based on deck requirements."""
print('Adding "standard" non-basics') logging.info('Adding staple non-basic lands')
self.staples = ['Reliquary Tower']
if 'Landfall' not in self.commander_tags:
self.staples.append('Ash Barrens')
if len(self.colors) > 1:
# Adding command Tower
self.staples.append('Command Tower')
# Adding Exotic Orchard
self.staples.append('Exotic Orchard')
if len(self.colors) <= 2:
self.staples.append('War Room')
if self.commander_power >= 5:
self.staples.append('Rogue\'s Passage')
for card in self.staples: # Define staple lands and their conditions
if card not in self.card_library: staple_lands = {
self.add_card(card, 'Land', None, 0) 'Reliquary Tower': lambda: True, # Always include
else: 'Ash Barrens': lambda: 'Landfall' not in self.commander_tags,
pass '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
}
lands_to_remove = self.staples self.staples = []
self.land_df = self.land_df[~self.land_df['name'].isin(lands_to_remove)] try:
self.land_df.to_csv(f'{csv_directory}/test_lands.csv', index=False) # 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}")
# 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)
logging.info(f'Added {len(self.staples)} staple lands')
except Exception as e:
logging.error(f"Error adding staple lands: {e}")
raise
def add_fetches(self): def add_fetches(self):
# Determine how many fetches in total # Determine how many fetches in total
print('How many fetch lands would you like to include?\n' print('How many fetch lands would you like to include?\n'
@ -1210,51 +1213,59 @@ class DeckBuilder:
self.land_df.to_csv(f'{csv_directory}/test_lands.csv', index=False) self.land_df.to_csv(f'{csv_directory}/test_lands.csv', index=False)
def add_kindred_lands(self): def add_kindred_lands(self):
print('Adding lands that care about the commander having a Kindred theme.') """Add lands that support tribal/kindred themes."""
logging.info('Adding general Kindred lands.') logging.info('Adding Kindred-themed lands')
def create_land(name: str, land_type: str) -> dict:
"""Helper function to create land card dictionaries"""
return {
'name': name,
'type': land_type,
'manaCost': None,
'manaValue': 0
}
kindred_lands = [ # Standard Kindred support lands
create_land('Path of Ancestry', 'Land'), KINDRED_STAPLES = [
create_land('Three Tree City', 'Legendary Land'), {'name': 'Path of Ancestry', 'type': 'Land'},
create_land('Cavern of Souls', 'Land') {'name': 'Three Tree City', 'type': 'Legendary Land'},
{'name': 'Cavern of Souls', 'type': 'Land'}
] ]
for theme in self.themes: kindred_lands = KINDRED_STAPLES.copy()
if 'Kindred' in theme: lands_to_remove = set()
kindred = theme.replace(' Kindred', '')
logging.info(f'Adding any {kindred}-specific lands.') try:
for _, row in self.land_df.iterrows(): # Process each Kindred theme
card = { for theme in self.themes:
'name': row['name'], if 'Kindred' in theme:
'type': row['type'], creature_type = theme.replace(' Kindred', '')
'manaCost': row['manaCost'], logging.info(f'Searching for {creature_type}-specific lands')
'manaValue': row['manaValue']
} # Filter lands by creature type
if pd.isna(row['text']): type_specific = self.land_df[
continue self.land_df['text'].notna() &
if pd.isna(row['type']): (self.land_df['text'].str.contains(creature_type, case=False) |
continue self.land_df['type'].str.contains(creature_type, case=False))
if (kindred in row['text']) or (kindred in row['type']): ]
kindred_lands.append(card)
# Add matching lands to pool
lands_to_remove = [] for _, row in type_specific.iterrows():
for card in kindred_lands: kindred_lands.append({
self.add_card(card['name'], card['type'], 'name': row['name'],
card['manaCost'], card['manaValue']) 'type': row['type'],
lands_to_remove.append(card['name']) 'manaCost': row['manaCost'],
'manaValue': row['manaValue']
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) 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
def add_dual_lands(self): def add_dual_lands(self):
# Determine dual-color lands available # Determine dual-color lands available
@ -1359,53 +1370,68 @@ class DeckBuilder:
logging.info('Skipping adding Triome land cards.') logging.info('Skipping adding Triome land cards.')
def add_misc_lands(self): def add_misc_lands(self):
print('Adding additional misc. lands to the deck that fit the color identity.') """Add additional utility lands that fit the deck's color identity."""
# Add other remaining lands that match color identity logging.info('Adding miscellaneous utility lands')
logging.info('Grabbing lands in your commander\'s color identity that aren\'t already in the deck.')
# Create a copy of land DataFrame and limit rows if needed for performance
land_df_misc = self.land_df.copy()
land_df_misc = land_df_misc.head(100) if len(land_df_misc) > 100 else land_df_misc
logging.debug(f"Land DataFrame contents:\n{land_df_misc}")
card_pool = []
for _, row in land_df_misc.iterrows():
card = {
'name': row['name'],
'type': row['type'],
'manaCost': row['manaCost'],
'manaValue': row['manaValue']
}
if card['name'] not in self.card_library['Card Name'].values:
card_pool.append(card)
# Add cards to the deck library
cards_to_add = []
while len(cards_to_add) < random.randint(5, 15):
card = random.choice(card_pool)
card_pool.remove(card)
# Check price constraints if enabled
if use_scrython and self.set_max_card_price:
price = self.price_check(card['name'])
if price > self.max_card_price * 1.1:
continue
# Add card if not already in library
if (card['name'] not in self.card_library['Card Name'].values):
cards_to_add.append(card)
# Add selected cards to library MIN_MISC_LANDS = 5
lands_to_remove = [] MAX_MISC_LANDS = 15
for card in cards_to_add: MAX_POOL_SIZE = 100
self.add_card(card['name'], card['type'],
card['manaCost'], card['manaValue']) try:
lands_to_remove.append(card['name']) # Create filtered pool of candidate lands
land_pool = (self.land_df
self.land_df = self.land_df[~self.land_df['name'].isin(lands_to_remove)] .head(MAX_POOL_SIZE)
self.land_df.to_csv(f'{csv_directory}/test_lands.csv', index=False) .copy()
.reset_index(drop=True))
logging.info(f'Added {len(cards_to_add)} land cards.')
# 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
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
def check_basics(self): def check_basics(self):
"""Check and display counts of each basic land type.""" """Check and display counts of each basic land type."""
basic_lands = { basic_lands = {
@ -1522,16 +1548,15 @@ class DeckBuilder:
logging.warning("Failed to remove land card.") logging.warning("Failed to remove land card.")
def count_pips(self): def count_pips(self):
"""Count and display the number of colored mana symbols in casting costs.""" """Count and display the number of colored mana symbols in casting costs using vectorized operations."""
logging.info('Analyzing color pip distribution...') logging.info('Analyzing color pip distribution...')
pip_counts = { # Define colors to check
'W': 0, 'U': 0, 'B': 0, 'R': 0, 'G': 0 colors = ['W', 'U', 'B', 'R', 'G']
}
for cost in self.card_library['Mana Cost'].dropna(): # Use vectorized string operations
for color in pip_counts: mana_costs = self.card_library['Mana Cost'].dropna()
pip_counts[color] += cost.count(color) pip_counts = {color: mana_costs.str.count(color).sum() for color in colors}
total_pips = sum(pip_counts.values()) total_pips = sum(pip_counts.values())
if total_pips == 0: if total_pips == 0: