mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-22 04:50:46 +02:00
Added more deck builder logic, including asking prelim questions and adding a land 'starter pack'
This commit is contained in:
parent
aee44190a9
commit
0ecf34210b
3 changed files with 313 additions and 25 deletions
274
deck_builder.py
274
deck_builder.py
|
@ -3,10 +3,19 @@ from __future__ import annotations
|
|||
import inquirer.prompt # type: ignore
|
||||
import pandas as pd # type: ignore
|
||||
import pprint # type: ignore
|
||||
import random
|
||||
|
||||
from fuzzywuzzy import fuzz, process # type: ignore
|
||||
from fuzzywuzzy import fuzz, process, utils # type: ignore
|
||||
from IPython.display import display
|
||||
|
||||
import settings
|
||||
|
||||
from setup import determine_legendary
|
||||
|
||||
pd.set_option('display.max_columns', None)
|
||||
pd.set_option('display.max_rows', None)
|
||||
pd.set_option('display.max_colwidth', 5)
|
||||
|
||||
# Basic deck builder, initial plan will just be for kindred support.
|
||||
# Would like to add logic for other themes, as well as automatically go
|
||||
# through the commander and find suitable themes.
|
||||
|
@ -23,6 +32,11 @@ class DeckBuilder:
|
|||
# Commander
|
||||
self.commander = ''
|
||||
self.commander_info = {}
|
||||
self.color_identity = ''
|
||||
self.colors = []
|
||||
self.creature_types = []
|
||||
self.info_tags = []
|
||||
self.commander_df = pd.DataFrame()
|
||||
|
||||
# Library (99 cards total)
|
||||
self.library = []
|
||||
|
@ -92,7 +106,11 @@ class DeckBuilder:
|
|||
card_choice = answer['card_prompt']
|
||||
|
||||
# Logic to find the card in the legendary_cards csv, then display it's information
|
||||
df = pd.read_csv('csv_files/legendary_cards.csv', low_memory=False)
|
||||
try:
|
||||
df = pd.read_csv('csv_files/legendary_cards.csv')
|
||||
except FileNotFoundError:
|
||||
determine_legendary()
|
||||
df = pd.read_csv('csv_files/legendary_cards.csv')
|
||||
fuzzy_card_choice = process.extractOne(card_choice, df['name'], scorer=fuzz.ratio)
|
||||
fuzzy_card_choice = fuzzy_card_choice[0]
|
||||
filtered_df = df[df['name'] == fuzzy_card_choice]
|
||||
|
@ -101,6 +119,7 @@ class DeckBuilder:
|
|||
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)
|
||||
|
||||
# Confirm if card entered was correct
|
||||
correct_commander = [
|
||||
|
@ -114,18 +133,259 @@ class DeckBuilder:
|
|||
if commander_confirmed:
|
||||
commander_chosen = True
|
||||
self.commander_info = df_dict
|
||||
first_key = list(self.commander_info.keys())[0]
|
||||
self.commander = str(self.commander_info[first_key])
|
||||
self.commander = self.commander_df.at[0, 'name']
|
||||
#print(self.commander)
|
||||
else:
|
||||
commander_chosen = False
|
||||
|
||||
# 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
|
||||
self.commander_keywords = df.at[0, 'keywords']
|
||||
self.commander_power = int(df.at[0, 'power'])
|
||||
self.commander_toughness = int(df.at[0, 'toughness'])
|
||||
self.commander_mana_cost = df.at[0, 'manaCost']
|
||||
|
||||
# Run the color setup
|
||||
self.set_color_identity(df)
|
||||
|
||||
# Run the creature type setup
|
||||
self.set_creature_types(df)
|
||||
|
||||
# Setup deck theme tags
|
||||
self.setup_deck_tags(df)
|
||||
|
||||
def set_color_identity(self, df):
|
||||
# Set color identity
|
||||
self.color_identity = df.at[0, 'colorIdentity'].split(', ')
|
||||
# Set creature colors
|
||||
self.colors = df.at[0, 'colors'].split(', ')
|
||||
|
||||
def set_creature_types(self, df):
|
||||
# Set creature types
|
||||
types = df.at[0, 'type']
|
||||
print(types)
|
||||
split_types = types.split()
|
||||
for type in split_types:
|
||||
if type not in settings.non_creature_types:
|
||||
self.creature_types.append(type)
|
||||
|
||||
def setup_deck_tags(self, df):
|
||||
# Determine card tags, such as counters theme
|
||||
"""keywords = df.at[0, 'keywords'].split()
|
||||
for keyword in keywords:
|
||||
settings.theme_tags.append(keyword.lower())
|
||||
print(settings.theme_tags)"""
|
||||
|
||||
self.check_tags(df.at[0, 'text'], settings.theme_tags)
|
||||
#print(card_tags)
|
||||
|
||||
def check_tags(self, string, word_list, threshold=80):
|
||||
card_tags = []
|
||||
for word in word_list:
|
||||
#print(word)
|
||||
if word == '+1/+1 counter' or word == '-1/-1 counter':
|
||||
threshold += 20
|
||||
if fuzz.partial_ratio(string.lower(), word.lower()) >= threshold:
|
||||
card_tags.append(word)
|
||||
#return True
|
||||
#return False
|
||||
self.commander_tags = card_tags
|
||||
|
||||
def determine_ideals(self):
|
||||
# "Free" slots that can be used for anything that isn't the ideals
|
||||
self.free_slots = 99
|
||||
|
||||
# Determine ideal land count
|
||||
print('How many lands would you like to include?\n'
|
||||
'Before ramp is taken into account, 38-40 would be "normal" for a deck.\n'
|
||||
'Broadly speaking, for every mana produced per 3 mana spent on ramp could reduce land count by 1.\n'
|
||||
'If you\'re playing landfall, probably consider 40 as baseline before ramp.')
|
||||
question = [
|
||||
inquirer.Text
|
||||
]
|
||||
inquirer.Text(
|
||||
'land_prompt',
|
||||
message=''
|
||||
)
|
||||
]
|
||||
answer = inquirer.prompt(question)
|
||||
self.ideal_land_count = int(answer['land_prompt'])
|
||||
self.free_slots -= self.ideal_land_count
|
||||
|
||||
# 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'
|
||||
'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')
|
||||
question = [
|
||||
inquirer.Text(
|
||||
'creature_prompt',
|
||||
message=''
|
||||
)
|
||||
]
|
||||
answer = inquirer.prompt(question)
|
||||
self.ideal_creature_count = int(answer['creature_prompt'])
|
||||
self.free_slots -= self.ideal_creature_count
|
||||
|
||||
# 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'
|
||||
'Counterspells can be consisdered proactive removal and protection.\n'
|
||||
'If you\'re going spellslinger, more would be a good idea as you might have less cretaures.')
|
||||
question = [
|
||||
inquirer.Text(
|
||||
'removal_prompt',
|
||||
message=''
|
||||
)
|
||||
]
|
||||
answer = inquirer.prompt(question)
|
||||
self.ideal_removal = int(answer['removal_prompt'])
|
||||
self.free_slots -= self.ideal_removal
|
||||
|
||||
# Determine board wipes
|
||||
print('How many board wipesyou like to include?\n'
|
||||
'Somewhere around 2-3 is good to help eliminate threats, but also prevent the game from running long\n.'
|
||||
'This can include damaging wipes like \'Blasphemous Act\' or toughness reduction like \'Meathook Massacre\'.')
|
||||
question = [
|
||||
inquirer.Text(
|
||||
'board_wipe_prompt',
|
||||
message=''
|
||||
)
|
||||
]
|
||||
answer = inquirer.prompt(question)
|
||||
self.ideal_wipes = int(answer['board_wipe_prompt'])
|
||||
self.free_slots -= self.ideal_wipes
|
||||
|
||||
# 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'
|
||||
'Try to have a majority of it be non-conditional, and only have a couple of \'Rhystic Study\' style effects.')
|
||||
question = [
|
||||
inquirer.Text(
|
||||
'draw_prompt',
|
||||
message=''
|
||||
)
|
||||
]
|
||||
answer = inquirer.prompt(question)
|
||||
self.ideal_card_advantage = int(answer['draw_prompt'])
|
||||
self.free_slots -= self.ideal_card_advantage
|
||||
|
||||
# Determine ramp
|
||||
print('How many pieces of ramp would you like to include?\n'
|
||||
'You\'re gonna want a decent amount of ramp, both getting lands or mana rocks/dorks.\n'
|
||||
'A good baseline is 8-12, scaling up with average CMC.')
|
||||
question = [
|
||||
inquirer.Text(
|
||||
'ramp_prompt',
|
||||
message=''
|
||||
)
|
||||
]
|
||||
answer = inquirer.prompt(question)
|
||||
self.ideal_ramp = int(answer['ramp_prompt'])
|
||||
self.free_slots -= self.ideal_ramp
|
||||
|
||||
# Determine how many protection spells
|
||||
print('How protection spells would you like to include?\n'
|
||||
'This can be individual protection, board protection, fogs, or similar effects.\n'
|
||||
'Things that grant indestructible, hexproof, phase out, or event just counterspells.\n'
|
||||
'This can be a widely variable ideal count, and can be as low as 5, and up past 15,\n'
|
||||
'it depends on your commander and how important your wincons are.')
|
||||
question = [
|
||||
inquirer.Text(
|
||||
'protection_prompt',
|
||||
message=''
|
||||
)
|
||||
]
|
||||
answer = inquirer.prompt(question)
|
||||
self.ideal_protection = int(answer['protection_prompt'])
|
||||
self.free_slots -= self.ideal_protection
|
||||
|
||||
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.')
|
||||
|
||||
def add_lands(self):
|
||||
# Begin the process to add lands, the number will depend on ideal land count, ramp,
|
||||
# and if any utility lands may be helpful.
|
||||
# By default, ({self.ideal_land_count} - 5) basic lands will be added, distributed
|
||||
# across the commander color identity. These will be removed for utility lands,
|
||||
# multi-color producing lands, fetches, and any MDFCs added later
|
||||
print(f'Adding {self.ideal_land_count} - 5 basic lands.')
|
||||
for color in self.color_identity:
|
||||
if color == 'W':
|
||||
basic = 'Plains'
|
||||
elif color == 'U':
|
||||
basic = 'Island'
|
||||
elif color == 'B':
|
||||
basic = 'Swamp',
|
||||
elif color == 'R':
|
||||
basic = 'Mountain'
|
||||
elif color == 'G':
|
||||
basic = 'Forest'
|
||||
"""if color =='':
|
||||
basic = 'Wastes'"""
|
||||
num_basics = self.ideal_land_count - 5
|
||||
for _ in range(num_basics // len(self.color_identity)):
|
||||
self.land_cards.append(basic)
|
||||
#print(self.land_cards)
|
||||
|
||||
# Add lands that are good in most any commander deck
|
||||
print('Adding \'standard\' non-basics')
|
||||
self.land_cards.append('Reliquary Tower')
|
||||
if 'landfall' not in self.commander_tags:
|
||||
self.land_cards.append('Ash Barrens')
|
||||
if len(self.color_identity) > 1:
|
||||
self.land_cards.append('Command Tower')
|
||||
self.land_cards.append('Exotic Orchard')
|
||||
self.land_cards.append('Evolving Wilds')
|
||||
if len(self.color_identity) <= 2:
|
||||
self.land_cards.append('War Room')
|
||||
if self.commander_power >= 5:
|
||||
self.land_cards.append('Rogue\'s Passage')
|
||||
|
||||
# If over ideal land count, remove random basics until ideal land count
|
||||
while len(self.land_cards) > self.ideal_land_count:
|
||||
self.remove_basic()
|
||||
|
||||
#if self.land_cards < self.ideal_land_count:
|
||||
# pass
|
||||
print(*self.land_cards, sep='\n')
|
||||
print(len(self.land_cards))
|
||||
|
||||
def remove_basic(self):
|
||||
basic_lands = []
|
||||
for color in self.color_identity:
|
||||
if color == 'W':
|
||||
basic = 'Plains'
|
||||
elif color == 'U':
|
||||
basic = 'Island'
|
||||
elif color == 'B':
|
||||
basic = 'Swamp',
|
||||
elif color == 'R':
|
||||
basic = 'Mountain'
|
||||
elif color == 'G':
|
||||
basic = 'Forest'
|
||||
if basic not in basic_lands:
|
||||
basic_lands.append(basic)
|
||||
|
||||
basic_land = random.choice(basic_lands)
|
||||
#print(basic_land)
|
||||
self.land_cards.remove(basic_land)
|
||||
|
||||
def add_creatures(self):
|
||||
# Begin the process to add creatures, the number added will depend on what the
|
||||
# deck plan is, the commander, creature types, etc...
|
||||
print(f'Adding the creatures to deck, a baseline based on the ideal creature count of {self.ideal_creature_count} will be used.')
|
||||
|
||||
build_deck = DeckBuilder()
|
||||
"""build_deck.determine_commander()
|
||||
print(f'Commander: {build_deck.commander}')
|
||||
print(f'Color Identity: {build_deck.color_identity}')
|
||||
print(f'Commander Colors: {build_deck.colors}')
|
||||
print(f'Commander Creature Types: {build_deck.creature_types}')
|
||||
print(f'Commander tags: {build_deck.commander_tags}')"""
|
||||
build_deck.determine_commander()
|
||||
print(build_deck.commander)
|
||||
build_deck.ideal_land_count = 35
|
||||
build_deck.add_lands()
|
44
settings.py
Normal file
44
settings.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
banned_cards = ['Ancestral Recall', 'Balance', 'Biorhythm', 'Black Lotus',
|
||||
'Braids, Cabal Minion', 'Chaos Orb', 'Coalition Victory',
|
||||
'Channel', 'Dockside Extortionist', 'Emrakul, the Aeons Torn',
|
||||
'Erayo, Soratami Ascendant', 'Falling Star', 'Fastbond',
|
||||
'Flash', 'Gifts Ungiven', 'Golos, Tireless Pilgrim',
|
||||
'Griselbrand', 'Hullbreacher', 'Iona, Shield of Emeria',
|
||||
'Karakas', 'Jeweled Lotus', 'Leovold, Emissary of Trest',
|
||||
'Library of Alexandria', 'Limited Resources', 'Lutri, the Spellchaser',
|
||||
'Mana Crypt', 'Mox Emerald', 'Mox Jet', 'Mox Pearl', 'Mox Ruby',
|
||||
'Mox Sapphire', 'Nadu, Winged Wisdom', 'Panoptic Mirror',
|
||||
'Paradox Engine', 'Primeval Titan', 'Prophet of Kruphix',
|
||||
'Recurring Nightmare', 'Rofellos, Llanowar Emissary', 'Shahrazad',
|
||||
'Sundering Titan', 'Sway of the Stars', 'Sylvan Primordial',
|
||||
'Time Vault', 'Time Walk', 'Tinker', 'Tolarian Academy',
|
||||
'Trade Secrets', 'Upheaval', 'Yawgmoth\'s Bargain']
|
||||
|
||||
non_creature_types = ['Legendary', 'Creature', 'Enchantment', 'Artifact',
|
||||
'Battle', 'Sorcery', 'Instant', 'Land', '-', '—',
|
||||
'Blood', 'Clue', 'Food', 'Gold', 'Incubator',
|
||||
'Junk', 'Map', 'Powerstone', 'Treasure',
|
||||
'Equipment', 'Fortification', 'vehicle',
|
||||
'Bobblehead', 'Attraction', 'Contraption',
|
||||
'Siege',
|
||||
'Aura', 'Background', 'Saga', 'Role', 'Shard',
|
||||
'Cartouche', 'Case', 'Class', 'Curse', 'Rune',
|
||||
'Shrine',
|
||||
'Plains', 'Island', 'Swamp', 'Forest', 'Mountain',
|
||||
'Cave', 'Desert', 'Gate', 'Lair', 'Locus', 'Mine',
|
||||
'Power-Plant', 'Sphere', 'Tower', 'Urza\'s']
|
||||
|
||||
theme_tags = ['+1/+1 counter', 'one or more counters', 'tokens', 'gain life', 'one or more creature tokens',
|
||||
'creature token', 'treasure', 'create token', 'draw a card', 'flash', 'choose a creature type',
|
||||
'play land', 'artifact you control enters', 'enchantment you control enters', 'poison counter',
|
||||
'from graveyard', 'mana value', 'from exile', 'mana of any color', 'attacks', 'total power',
|
||||
'greater than starting life', 'lose life', 'whenever you sacrifice', 'creature dying',
|
||||
'creature enters', 'creature leaves', 'creature dies', 'put into graveyard', 'sacrifice',
|
||||
'sacricifice creature', 'sacrifice artifact', 'sacrifice another creature', '-1/-1 counter',
|
||||
'control get +1/+1', 'control dies', 'experience counter', 'triggered ability', 'token']
|
||||
|
||||
board_wipe_tags = ['destroy all', 'destroy each', 'return all', 'return each', 'deals damage to each',
|
||||
'exile all', 'exile each', 'creatures get -X/-X', 'sacrifices all', 'sacrifices each',
|
||||
'sacrifices the rest']
|
||||
targetted_removal_tags = ['exile target', 'destroy target', 'return target', 'shuffles target', 'you control',
|
||||
'deals damage to target','loses all abilities']
|
20
setup.py
20
setup.py
|
@ -3,29 +3,14 @@ from __future__ import annotations
|
|||
import pandas as pd # type: ignore
|
||||
import requests # type: ignore
|
||||
|
||||
from settings import banned_cards
|
||||
|
||||
staple_lists = ['Colorless', 'White', 'Blue', 'Black']
|
||||
colorless_staples = [] # type: ignore
|
||||
white_staples = [] # type: ignore
|
||||
blue_staples = [] # type: ignore
|
||||
black_staples = [] # type: ignore
|
||||
|
||||
banned_cards = ['Ancestral Recall', 'Balance', 'Biorhythm', 'Black Lotus',
|
||||
'Braids, Cabal Minion', 'Chaos Orb', 'Coalition Victory',
|
||||
'Channel', 'Dockside Extortionist', 'Emrakul, the Aeons Torn',
|
||||
'Erayo, Soratami Ascendant', 'Falling Star', 'Fastbond',
|
||||
'Flash', 'Gifts Ungiven', 'Golos, Tireless Pilgrim',
|
||||
'Griselbrand', 'Hullbreacher', 'Iona, Shield of Emeria',
|
||||
'Karakas', 'Jeweled Lotus', 'Leovold, Emissary of Trest',
|
||||
'Library of Alexandria', 'Limited Resources', 'Lutri, the Spellchaser',
|
||||
'Mana Crypt', 'Mox Emerald', 'Mox Jet', 'Mox Pearl', 'Mox Ruby',
|
||||
'Mox Sapphire', 'Nadu, Winged Wisdom', 'Panoptic Mirror',
|
||||
'Paradox Engine', 'Primeval Titan', 'Prophet of Kruphix',
|
||||
'Recurring Nightmare', 'Rofellos, Llanowar Emissary', 'Shahrazad',
|
||||
'Sundering Titan', 'Sway of the Stars', 'Sylvan Primordial',
|
||||
'Time Vault', 'Time Walk', 'Tinker', 'Tolarian Academy',
|
||||
'Trade Secrets', 'Upheaval', 'Yawgmoth\'s Bargain']
|
||||
|
||||
|
||||
def filter_by_color(df, column_name, value, new_csv_name):
|
||||
# Filter dataframe
|
||||
filtered_df = df[df[column_name] == value]
|
||||
|
@ -653,4 +638,3 @@ def generate_staple_lists():
|
|||
for items in staples:
|
||||
f.write('%s\n' %items)
|
||||
|
||||
determine_legendary()
|
Loading…
Add table
Add a link
Reference in a new issue