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'
self.files_to_load = ['colorless', 'black']
elif self.color_identity == 'G':
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 except Exception as e:
elif self.color_identity == 'B, G': logging.error(f"Error in determine_color_identity: {e}")
self.color_identity_full = 'Golgari: Black/Green' raise
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
@ -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
self.add_basics()
self.check_basics()
self.add_standard_non_basics()
self.add_fetches()
for index, row in self.land_df.iterrows(): # Add theme and color-specific lands
for land in self.card_library: if any('Kindred' in theme for theme in self.themes):
if land in row['name']: self.add_kindred_lands()
self.drop_card(self.land_df, index) if len(self.colors) >= 2:
self.add_dual_lands()
if len(self.colors) >= 3:
self.add_triple_lands()
self.land_df.to_csv(f'{csv_directory}/test_lands.csv', index=False) self.add_misc_lands()
# If over ideal land count, remove random basics until ideal land count # Clean up land database
self.check_basics() mask = self.land_df['name'].isin(self.card_library['Card Name'])
logging.info('Checking total land count to ensure it\'s within ideal count.\n\n') self.land_df = self.land_df[~mask]
self.organize_library() self.land_df.to_csv(f'{csv_directory}/test_lands.csv', index=False)
while self.land_cards > int(self.ideal_land_count):
logging.info(f'Total lands: {self.land_cards}') # Adjust to ideal land count
logging.info(f'Ideal lands: {self.ideal_land_count}') self.check_basics()
self.remove_basic() 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 # Define staple lands and their conditions
self.staples.append('Exotic Orchard') 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
}
if len(self.colors) <= 2: self.staples = []
self.staples.append('War Room') 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}")
if self.commander_power >= 5: # Update land database
self.staples.append('Rogue\'s Passage') 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)
for card in self.staples: logging.info(f'Added {len(self.staples)} staple lands')
if card not in self.card_library:
self.add_card(card, 'Land', None, 0)
else:
pass
lands_to_remove = self.staples
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)
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: # Standard Kindred support lands
"""Helper function to create land card dictionaries""" KINDRED_STAPLES = [
return { {'name': 'Path of Ancestry', 'type': 'Land'},
'name': name, {'name': 'Three Tree City', 'type': 'Legendary Land'},
'type': land_type, {'name': 'Cavern of Souls', 'type': 'Land'}
'manaCost': None,
'manaValue': 0
}
kindred_lands = [
create_land('Path of Ancestry', 'Land'),
create_land('Three Tree City', 'Legendary Land'),
create_land('Cavern of Souls', '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.')
for _, row in self.land_df.iterrows():
card = {
'name': row['name'],
'type': row['type'],
'manaCost': row['manaCost'],
'manaValue': row['manaValue']
}
if pd.isna(row['text']):
continue
if pd.isna(row['type']):
continue
if (kindred in row['text']) or (kindred in row['type']):
kindred_lands.append(card)
lands_to_remove = [] try:
for card in kindred_lands: # Process each Kindred theme
self.add_card(card['name'], card['type'], for theme in self.themes:
card['manaCost'], card['manaValue']) if 'Kindred' in theme:
lands_to_remove.append(card['name']) creature_type = theme.replace(' Kindred', '')
logging.info(f'Searching for {creature_type}-specific lands')
self.land_df = self.land_df[~self.land_df['name'].isin(lands_to_remove)] # Filter lands by creature type
self.land_df.to_csv(f'{csv_directory}/test_lands.csv', index=False) 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
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.') MIN_MISC_LANDS = 5
# Create a copy of land DataFrame and limit rows if needed for performance MAX_MISC_LANDS = 15
land_df_misc = self.land_df.copy() MAX_POOL_SIZE = 100
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 = [] try:
for _, row in land_df_misc.iterrows(): # Create filtered pool of candidate lands
card = { land_pool = (self.land_df
'name': row['name'], .head(MAX_POOL_SIZE)
'type': row['type'], .copy()
'manaCost': row['manaCost'], .reset_index(drop=True))
'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): # Convert to card dictionaries
card = random.choice(card_pool) card_pool = [
card_pool.remove(card) {
'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
# 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) cards_to_add.append(card)
# Add selected cards to library # Add selected lands
lands_to_remove = [] lands_to_remove = set()
for card in cards_to_add: for card in cards_to_add:
self.add_card(card['name'], card['type'], self.add_card(card['name'], card['type'],
card['manaCost'], card['manaValue']) card['manaCost'], card['manaValue'])
lands_to_remove.append(card['name']) lands_to_remove.add(card['name'])
self.land_df = self.land_df[~self.land_df['name'].isin(lands_to_remove)] # Update land database
self.land_df.to_csv(f'{csv_directory}/test_lands.csv', index=False) 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)} land cards.') 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: