diff --git a/Installer.iss b/Installer.iss index c8cec9d..39686ec 100644 --- a/Installer.iss +++ b/Installer.iss @@ -1,7 +1,7 @@ ; -- MtgaDraft.iss -- [Setup] AppName=MTGA Draft Tool -AppVersion=3.02 +AppVersion=3.03 WizardStyle=modern DefaultDirName={sd}\MtgaDraftTool DefaultGroupName=MtgaDraftTool diff --git a/README.md b/README.md index c28ea54..06c58e6 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Magic: The Gathering Arena draft tool that utilizes 17Lands data. - This step isn't necessary if the application is installed in the main directory of a drive (i.e., `C:/` or `D:/`) or the `Users//` directory - **Step 5:** Double-click the `MTGA_Draft_Tool.exe` to start the program. + - If you're running a higher resolution than 1920x1080, then you might want to consider modifying the scale_factor field in the config.json file - **Step 6:** Download the sets that you plan on using (`Data->View Sets`). @@ -46,6 +47,7 @@ Magic: The Gathering Arena draft tool that utilizes 17Lands data. - **Step 6:** (Mac Only) Install web certificates by going to `/Applications/Python 3.10/` and double-clicking the file `Install Certificates.command`. - **Step 7:** Start the application by opening the terminal and entering ```python3.10 main.py```. + - If you're running a higher resolution than 1920x1080, then you might want to consider modifying the scale_factor field in the config.json file - **Step 8:** (Mac Only) Set Arena to window mode. @@ -136,6 +138,8 @@ Magic: The Gathering Arena draft tool that utilizes 17Lands data. - **Enable Card Colors:** Sets the row color to the card color. +- **Enable Color Identity:** When enabled, the Colors field will display the mana symbols for a card's mana cost AND abilities (i.e., kicker, activated abilities, etc.) + - **Enable Draft Stats:** Displays the draft stats table and drop-down in the main window. - **Enable Missing Cards:** Displays the missing cards table in the main window. @@ -167,14 +171,14 @@ Magic: The Gathering Arena draft tool that utilizes 17Lands data. | D- | >= -1.33 | | F | < -1.33 | -- **Win Rate Ratings:** The application will calculate the mean and standard deviation to identify an upper and lower limit (-1.67 to 2 standard deviations from the mean) and perform the following calculation to determine a card's rating: `((card_gihwr - lower_limit) / (upper_limit - lower_limit)) * 5.0` - - Example: If the calculated mean and standard deviation for a set are 56.8% and 4.68, then the upper limit will be `56.8 + 2 * 4.68 = 66.16%`, the lower limit will be `56.8 - 1.67 * 4.68 = 48.98%`, and the resulting rating for a card with a win rate of 62% will be `(((62 - 48.98) / (66.16 - 48.98)) * 5.0 = 3.8)` +- **Win Rate Ratings:** The application will calculate the mean and standard deviation to identify an upper and lower limit (-1.33 to 2.33 standard deviations from the mean) and perform the following calculation to determine a card's rating: `((card_gihwr - lower_limit) / (upper_limit - lower_limit)) * 5.0` + - Example: If the calculated mean and standard deviation for a set are 56.8% and 4.68, then the upper limit will be `56.8 + 2.33 * 4.68 = 67.7%`, the lower limit will be `56.8 - 1.33 * 4.68 = 50.57%`, and the resulting rating for a card with a win rate of 62% will be `(((62 - 50.57) / (67.7 - 50.57)) * 5.0 = 3.3)` - **Bayesian Average:** A Bayesian average calculation applied to all win rate data based on some assumptions (expected range of 40-60% with a mean of 50%). - Enabled: The application will perform this calculation on all win rate data. The adjustment made by this calculation will disappear as the sample count (e.g, Number of Games In Hand for the Games in Hand Win Rate) reaches 200. - Disabled: The application will not perform this calculation. If the sample count is fewer than 200, then the application will set the win rate to 0 (same as the 17Lands Card Ratings table). -- **Auto Highest Rating:** If the `Auto` filter is set, and the user has taken at least 16 cards, then the application will try and determine the leading color pair from the taken cards. If the tool is unable to identify a definitive leading color pair, then it will display the highest win rate of the top two color pairs for each win rate field (e.g., GIHWR, OHWR, etc.). The filter label will display both color pairs separated by a slash (e.g., `Auto (WB/BG)`). +- **Auto Highest Rating:** If the `Auto` filter is set, and the user has taken at least 16 cards, then the application will try and determine the leading color combination from the taken cards. If the tool is unable to identify a definitive leading color pair, then it will display the highest win rate of the top two color combinations for each win rate field (e.g., GIHWR, OHWR, etc.). The filter label will display both color combinations separated by a slash (e.g., `Auto (WB/UBG)`). - Example: If the user has taken primarily black, blue, and green cards, and Generous Visitor has a BG win rate of 66% and a UB rating of 15%, then the displayed win rate will be 66%. - **Deck Suggester:** For each viable color combination, the deck suggester will construct multiple decks (Aggro, Mid, and Control decks), using some generic deck building requirements, from a card pool of the highest win rate cards. The suggester will rate each deck and choose the highest rated deck for each viable color combination. The deck suggester will NOT identify card synergies and build an intentionally synergistic deck. diff --git a/card_logic.py b/card_logic.py index b9f983d..0ff9d5e 100644 --- a/card_logic.py +++ b/card_logic.py @@ -1,61 +1,292 @@ +"""This module contains the functions that are used for processing the collected cards""" +from itertools import combinations +from dataclasses import dataclass, asdict, field import json import logging -import constants import math -import log_scanner as LS -from itertools import combinations -from dataclasses import dataclass, asdict +import numpy +import constants logic_logger = logging.getLogger(constants.LOG_TYPE_DEBUG) -@dataclass + +@dataclass +class DeckMetrics: + cmc_average: float = 0.0 + creature_count: int = 0 + noncreature_count: int = 0 + total_cards: int = 0 + total_non_land_cards: int = 0 + distribution_creatures: list = field( + default_factory=lambda: [0, 0, 0, 0, 0, 0, 0]) + distribution_noncreatures: list = field( + default_factory=lambda: [0, 0, 0, 0, 0, 0, 0]) + distribution_all: list = field( + default_factory=lambda: [0, 0, 0, 0, 0, 0, 0]) + + +@dataclass +class SetMetrics: + mean: float = 0.0 + standard_deviation: float = 0.0 + + +@dataclass class DeckType: + """This class holds the data for the various deck types (Aggro, Mid, and Control)""" distribution: list maximum_card_count: int recommended_creature_count: int - cmc_average : float + cmc_average: float + @dataclass class Config: - table_width : int=270 - column_2 : str=constants.COLUMNS_OPTIONS_MAIN_DICT[constants.COLUMN_2_DEFAULT] - column_3 : str=constants.COLUMNS_OPTIONS_MAIN_DICT[constants.COLUMN_3_DEFAULT] - column_4 : str=constants.COLUMNS_OPTIONS_MAIN_DICT[constants.COLUMN_4_DEFAULT] - column_5 : str=constants.COLUMNS_OPTIONS_EXTRA_DICT[constants.COLUMN_5_DEFAULT] - column_6 : str=constants.COLUMNS_OPTIONS_EXTRA_DICT[constants.COLUMN_6_DEFAULT] - column_7 : str=constants.COLUMNS_OPTIONS_EXTRA_DICT[constants.COLUMN_7_DEFAULT] - deck_filter : str=constants.DECK_FILTER_DEFAULT - filter_format : str=constants.DECK_FILTER_FORMAT_COLORS - result_format : str=constants.RESULT_FORMAT_WIN_RATE - card_colors_enabled: bool=False - missing_enabled : bool=True - stats_enabled : bool=False - hotkey_enabled : bool=True - images_enabled : bool=True - auto_highest_enabled : bool=True - curve_bonus_enabled : bool=False - color_bonus_enabled : bool=False - bayesian_average_enabled : bool=False - draft_log_enabled: bool=False - taken_alsa_enabled: bool=False - taken_ata_enabled: bool=False - taken_gpwr_enabled: bool=False - taken_ohwr_enabled: bool=False - taken_gndwr_enabled: bool=False - taken_iwd_enabled: bool=False - minimum_creatures : int=13 - minimum_noncreatures : int=6 - ratings_threshold : int=500 - alsa_weight : float=0.0 - iwd_weight :float=0.0 - - deck_mid : DeckType=DeckType([0,0,4,3,2,1,0], 23, 15, 3.04) - deck_aggro : DeckType=DeckType([0,2,5,3,0,0,0], 24, 17, 2.40) - deck_control : DeckType=DeckType([0,0,3,3,3,1,1], 22, 14, 3.68) + """This class holds the data that's stored in the config.json file""" + table_width: int = 270 + column_2: str = constants.COLUMNS_OPTIONS_MAIN_DICT[constants.COLUMN_2_DEFAULT] + column_3: str = constants.COLUMNS_OPTIONS_MAIN_DICT[constants.COLUMN_3_DEFAULT] + column_4: str = constants.COLUMNS_OPTIONS_MAIN_DICT[constants.COLUMN_4_DEFAULT] + column_5: str = constants.COLUMNS_OPTIONS_EXTRA_DICT[constants.COLUMN_5_DEFAULT] + column_6: str = constants.COLUMNS_OPTIONS_EXTRA_DICT[constants.COLUMN_6_DEFAULT] + column_7: str = constants.COLUMNS_OPTIONS_EXTRA_DICT[constants.COLUMN_7_DEFAULT] + deck_filter: str = constants.DECK_FILTER_DEFAULT + filter_format: str = constants.DECK_FILTER_FORMAT_COLORS + result_format: str = constants.RESULT_FORMAT_WIN_RATE + card_colors_enabled: bool = False + missing_enabled: bool = True + stats_enabled: bool = False + hotkey_enabled: bool = True + images_enabled: bool = True + auto_highest_enabled: bool = True + curve_bonus_enabled: bool = False + color_bonus_enabled: bool = False + bayesian_average_enabled: bool = False + draft_log_enabled: bool = False + color_identity_enabled: bool = False + taken_alsa_enabled: bool = False + taken_ata_enabled: bool = False + taken_gpwr_enabled: bool = False + taken_ohwr_enabled: bool = False + taken_gdwr_enabled: bool = False + taken_gndwr_enabled: bool = False + taken_iwd_enabled: bool = False + minimum_creatures: int = 13 + minimum_noncreatures: int = 6 + ratings_threshold: int = 500 + alsa_weight: float = 0.0 + iwd_weight: float = 0.0 + scale_factor: float = 1.0 + + deck_mid: DeckType = DeckType([0, 0, 4, 3, 2, 1, 0], 23, 15, 3.04) + deck_aggro: DeckType = DeckType([0, 2, 5, 3, 0, 0, 0], 24, 17, 2.40) + deck_control: DeckType = DeckType([0, 0, 3, 3, 3, 1, 1], 22, 14, 3.68) + + database_size: int = 0 + + +class CardResult: + """This class processes a card list and produces results based on a list of fields (i.e., ALSA, GIHWR, COLORS, etc.)""" + + def __init__(self, set_metrics, tier_data, configuration, pick_number): + self.metrics = set_metrics + self.tier_data = tier_data + self.configuration = configuration + self.pick_number = pick_number + + def return_results(self, card_list, colors, fields): + """This function processes a card list and returns a list with the requested field results""" + return_list = [] + wheel_sum = 0 + if constants.DATA_FIELD_WHEEL in fields.values(): + wheel_sum = self._retrieve_wheel_sum(card_list) + + for card in card_list: + try: + selected_card = card + selected_card["results"] = ["NA"] * len(fields) + + for count, option in enumerate(fields.values()): + if constants.FILTER_OPTION_TIER in option: + selected_card["results"][count] = self._process_tier( + card, option) + elif option == constants.DATA_FIELD_COLORS: + selected_card["results"][count] = self._process_colors( + card) + elif option == constants.DATA_FIELD_WHEEL: + selected_card["results"][count] = self._process_wheel_normalized( + card, wheel_sum) + elif option in card: + selected_card["results"][count] = card[option] + else: + selected_card["results"][count] = self._process_filter_fields( + card, option, colors) + + return_list.append(selected_card) + except Exception as error: + logic_logger.info("return_results error: %s", error) + return return_list + + def _process_tier(self, card, option): + """Retrieve tier list rating for this card""" + result = "NA" + try: + card_name = card[constants.DATA_FIELD_NAME].split(" // ") + if card_name[0] in self.tier_data[option][constants.DATA_SECTION_RATINGS]: + result = self.tier_data[option][constants.DATA_SECTION_RATINGS][card_name[0]] + except Exception as error: + logic_logger.info("_process_tier error: %s", error) + + return result + + def _process_colors(self, card): + """Retrieve card colors based on color identity (includes kicker, abilities, etc.) or mana cost""" + result = "NA" + + try: + if self.configuration.color_identity_enabled: + result = "".join(card[constants.DATA_FIELD_COLORS]) + elif constants.CARD_TYPE_LAND in card[constants.DATA_FIELD_TYPES]: + # For lands, the card mana cost can't be used to identify the card colors + result = "".join(card[constants.DATA_FIELD_COLORS]) + else: + result = "".join( + list(card_colors(card[constants.DATA_FIELD_MANA_COST]).keys())) + except Exception as error: + logic_logger.info("_process_colors error: %s", error) + + return result + + def _retrieve_wheel_sum(self, card_list): + """Calculate the sum of all wheel percentage values for the card list""" + total_sum = 0 + + for card in card_list: + total_sum += self._process_wheel(card) + + return total_sum + + def _process_wheel(self, card): + """Calculate wheel percentage""" + result = 0 - database_size : int=0 + try: + if self.pick_number <= len(constants.WHEEL_COEFFICIENTS): + # 0 is treated as pick 1 for PremierDraft P1P1 + self.pick_number = max(self.pick_number, 1) + alsa = card[constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_ALSA] + coefficients = constants.WHEEL_COEFFICIENTS[self.pick_number - 1] + # Exclude ALSA values below 2 + result = round(numpy.polyval(coefficients, alsa), + 1) if alsa >= 2 else 0 + result = max(result, 0) + except Exception as error: + logic_logger.info("_process_wheel error: %s", error) + + return result + + def _process_wheel_normalized(self, card, total_sum): + """Calculate the normalized wheel percentage using the sum of all percentages within the card list""" + result = 0 + + try: + result = self._process_wheel(card) + + result = round((result / total_sum)*100, 1) if total_sum > 0 else 0 + except Exception as error: + logic_logger.info("_process_wheel_normalized error: %s", error) + + return result + + def _process_filter_fields(self, card, option, colors): + """Retrieve win rate result based on the application settings""" + result = "NA" + + try: + rated_colors = [] + for color in colors: + if option in card[constants.DATA_FIELD_DECK_COLORS][color]: + if option in constants.WIN_RATE_OPTIONS: + rating_data = self._format_win_rate(card, + option, + constants.WIN_RATE_FIELDS_DICT[option], + color) + rated_colors.append(rating_data) + else: # Field that's not a win rate (ALSA, IWD, etc) + result = card[constants.DATA_FIELD_DECK_COLORS][color][option] + if rated_colors: + result = sorted( + rated_colors, key=field_process_sort, reverse=True)[0] + except Exception as error: + logic_logger.info("_process_filter_fields error: %s", error) + + return result + + def _format_win_rate(self, card, winrate_field, winrate_count, color): + """The function will return a grade, rating, or win rate depending on the application's Result Format setting""" + result = 0 + # Produce a result that matches the Result Format setting + if self.configuration.result_format == constants.RESULT_FORMAT_RATING: + result = self._card_rating( + card, winrate_field, winrate_count, color) + elif self.configuration.result_format == constants.RESULT_FORMAT_GRADE: + result = self._card_grade( + card, winrate_field, winrate_count, color) + else: + result = calculate_win_rate(card[constants.DATA_FIELD_DECK_COLORS][color][winrate_field], + card[constants.DATA_FIELD_DECK_COLORS][color][winrate_count], + self.configuration.bayesian_average_enabled) + + return result + + def _card_rating(self, card, winrate_field, winrate_count, color): + """The function will take a card's win rate and calculate a 5-point rating""" + result = 0 + try: + winrate = calculate_win_rate(card[constants.DATA_FIELD_DECK_COLORS][color][winrate_field], + card[constants.DATA_FIELD_DECK_COLORS][color][winrate_count], + self.configuration.bayesian_average_enabled) + + deviation_list = list(constants.GRADE_DEVIATION_DICT.values()) + upper_limit = self.metrics.mean + \ + self.metrics.standard_deviation * deviation_list[0] + lower_limit = self.metrics.mean + \ + self.metrics.standard_deviation * deviation_list[-1] + + if (winrate != 0) and (upper_limit != lower_limit): + result = round( + ((winrate - lower_limit) / (upper_limit - lower_limit)) * 5.0, 1) + result = min(result, 5.0) + result = max(result, 0) + + except Exception as error: + logic_logger.info("_card_rating error: %s", error) + return result + + def _card_grade(self, card, winrate_field, winrate_count, color): + """The function will take a card's win rate and assign a letter grade based on the number of standard deviations from the mean""" + result = constants.LETTER_GRADE_NA + try: + winrate = calculate_win_rate(card[constants.DATA_FIELD_DECK_COLORS][color][winrate_field], + card[constants.DATA_FIELD_DECK_COLORS][color][winrate_count], + self.configuration.bayesian_average_enabled) + + if ((winrate != 0) and (self.metrics.standard_deviation != 0)): + result = constants.LETTER_GRADE_F + for grade, deviation in constants.GRADE_DEVIATION_DICT.items(): + standard_score = ( + winrate - self.metrics.mean) / self.metrics.standard_deviation + if standard_score >= deviation: + result = grade + break + + except Exception as error: + logic_logger.info("_card_grade error: %s", error) + return result + -def FieldProcessSort(field_value): +def field_process_sort(field_value): + """This function collects the numeric order of a letter grade for the purpose of sorting""" processed_value = field_value try: @@ -65,226 +296,163 @@ def FieldProcessSort(field_value): pass return processed_value -def FormatTierResults(value, old_format, new_format): + +def format_tier_results(value, old_format, new_format): + """This function converts the tier list ratings, from old tier lists, back to letter grades""" new_value = value try: - #ratings to grades + # ratings to grades if (old_format == constants.RESULT_FORMAT_RATING) and (new_format == constants.RESULT_FORMAT_GRADE): - new_value = constants.LETTER_GRADE_F + new_value = constants.LETTER_GRADE_NA for grade, threshold in constants.TIER_CONVERSION_RATINGS_GRADES_DICT.items(): if value > threshold: new_value = grade break except Exception as error: - logic_logger.info(f"FormatTierResults Error: {error}") + logic_logger.info("format_tier_results error: %s", error) return new_value -def CompareRatings(a, b): - try: - if(a["rating_filter_c"] == b["rating_filter_c"]): - return a[constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_ALSA] - b[constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_ALSA] - else: - return b["rating_filter_c"] - a["rating_filter_c"] - except Exception as error: - logic_logger.info(f"CompareRatings Error: {error}") - return 0 - -def ColorAffinity(colors, card): - rating = card["rating"] - if(rating >= 1.5): - for color in card[constants.DATA_FIELD_COLORS]: - if (color not in colors): - colors[color] = 0 - colors[color] += rating - - return colors - -def ColorBonus (deck, deck_colors, card, bayesian_enabled): - - color_bonus_factor = 0.0 - color_bonus_level = 0.0 - search_colors = "" - combined_colors = "".join(deck_colors) - combined_colors = "".join(set(combined_colors)) - try: - card_colors = card[constants.DATA_FIELD_COLORS] - if(len(card_colors) == 0): - color_bonus_factor = 0.5 - search_colors = list(deck_colors)[0] - else: - matching_colors = list(filter((lambda x : x in combined_colors), card_colors)) - color_bonus_factor = len(matching_colors) / len(card_colors) - search_colors = matching_colors - - searched_cards = DeckColorSearch(deck, search_colors, constants.CARD_TYPE_DICT[constants.CARD_TYPE_SELECTION_ALL], True, False, True) - for card in searched_cards: - gihwr = CalculateWinRate(card[constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_GIHWR], - card[constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_GIH], - bayesian_enabled) - if gihwr >= 65.0: - color_bonus_level += 0.3 - elif gihwr >= 60.0: - color_bonus_level += 0.2 - elif gihwr >= 52.0: - color_bonus_level += 0.1 - color_bonus_level = min(color_bonus_level, 1) - - except Exception as error: - logic_logger.info(f"ColorBonus Error: {error}") - - return round(color_bonus_factor * color_bonus_level,1) - -def CurveBonus(deck, card, pick_number, color_filter, configuration): - curve_bonus_levels = [0.1, 0.1, 0.1, 0.1, 0.1, - 0.2, 0.2, 0.2, 0.2, 0.2, - 0.3, 0.3, 0.3, 0.5, 0.5, - 0.6, 0.6, 1.0, 1.0, 1.0] - curve_start = 15 - index = max(pick_number - curve_start, 0) - curve_bonus = 0.0 - curve_bonus_factor = 0.0 - minimum_creature_count = configuration.minimum_creatures - minimum_distribution = configuration.deck_mid.distribution - - try: - matching_colors = list(filter((lambda x : x in color_filter), card[constants.DATA_FIELD_COLORS])) - - if len(matching_colors) or len(card[constants.DATA_FIELD_COLORS]) == 0: - if any(x in card[constants.DATA_FIELD_TYPES] for x in constants.CARD_TYPE_DICT[constants.CARD_TYPE_SELECTION_CREATURES]): - card_list = DeckColorSearch(deck, color_filter, constants.CARD_TYPE_DICT[constants.CARD_TYPE_SELECTION_CREATURES], True, True, False) - for card in card_list: - card[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIHWR] = CalculateWinRate(card[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIHWR], - card[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIH], - configuration.bayesian_average_enabled) - #card_list = [CalculateWinRate(x[constants.DATA_FIELD_DECK_COLORS][color_filter], configuration.bayesian_average_enabled) for x in card_list] - card_colors_sorted = sorted(card_list, key = lambda k: k[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIHWR], reverse = True) - - cmc_total, count, distribution = ColorCmc(card_colors_sorted) - curve_bonus = curve_bonus_levels[int(min(index, len(curve_bonus_levels) - 1))] - - curve_bonus_factor = 1 - if(count > minimum_creature_count): - card_gihwr = CalculateWinRate(card[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIHWR], - card[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIH], - configuration.bayesian_average_enabled) - replaceable = [x for x in card_colors_sorted if (card[constants.DATA_FIELD_CMC] <= x[constants.DATA_FIELD_CMC] and (card_gihwr > x[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIHWR]))] - curve_bonus_factor = 0 - if len(replaceable): - index = int(min(card[constants.DATA_FIELD_CMC], len(distribution) - 1)) - - if(distribution[index] < minimum_distribution[index]): - curve_bonus_factor = 0.5 - else: - curve_bonus_factor = 0.25 - except Exception as error: - logic_logger.info(f"CurveBonus Error: {error}") - - return curve_bonus * curve_bonus_factor - -def DeckColorSearch(deck, search_colors, card_types, include_types, include_colorless, include_partial): + +def deck_card_search(deck, search_colors, card_types, include_types, include_colorless, include_partial): + """This function retrieves a subset of cards that meet certain criteria (type, color, etc.)""" card_color_sorted = {} main_color = "" combined_cards = [] for card in deck: try: - card_colors = CardColors(card["mana_cost"]) + colors = list(card_colors( + card[constants.DATA_FIELD_MANA_COST]).keys()) - if not card_colors: - card_colors = card[constants.DATA_FIELD_COLORS] + if constants.CARD_TYPE_LAND in card[constants.DATA_FIELD_TYPES]: + colors = card[constants.DATA_FIELD_COLORS] - if bool(card_colors) and (set(card_colors) <= set(search_colors)): - main_color = card_colors[0] + if colors and (set(colors) <= set(search_colors)): + main_color = colors[0] - if((include_types and any(x in card[constants.DATA_FIELD_TYPES][0] for x in card_types)) or - (not include_types and not any(x in card[constants.DATA_FIELD_TYPES][0] for x in card_types))): + if ((include_types and any(x in card[constants.DATA_FIELD_TYPES] for x in card_types)) or + (not include_types and not any(x in card[constants.DATA_FIELD_TYPES] for x in card_types))): - if main_color not in card_color_sorted.keys(): + if main_color not in card_color_sorted: card_color_sorted[main_color] = [] - + card_color_sorted[main_color].append(card) - elif set(search_colors).intersection(card_colors) and include_partial: - for color in card_colors: - if((include_types and any(x in card[constants.DATA_FIELD_TYPES][0] for x in card_types)) or - (not include_types and not any(x in card[constants.DATA_FIELD_TYPES][0] for x in card_types))): - - if color not in card_color_sorted.keys(): + elif set(search_colors).intersection(colors) and include_partial: + for color in colors: + if ((include_types and any(x in card[constants.DATA_FIELD_TYPES] for x in card_types)) or + (not include_types and not any(x in card[constants.DATA_FIELD_TYPES] for x in card_types))): + + if color not in card_color_sorted: card_color_sorted[color] = [] - + card_color_sorted[color].append(card) - if (bool(card_colors) == False) and include_colorless: - - if((include_types and any(x in card[constants.DATA_FIELD_TYPES][0] for x in card_types)) or - (not include_types and not any(x in card[constants.DATA_FIELD_TYPES][0] for x in card_types))): + if not colors and include_colorless: + + if ((include_types and any(x in card[constants.DATA_FIELD_TYPES] for x in card_types)) or + (not include_types and not any(x in card[constants.DATA_FIELD_TYPES] for x in card_types))): combined_cards.append(card) except Exception as error: - logic_logger.info(f"DeckColorSearch Error: {error}") + logic_logger.info("deck_card_search error: %s", error) + + for key, value in card_color_sorted.items(): + if key in search_colors: + combined_cards.extend(value) - for color in card_color_sorted: - if color in search_colors: - combined_cards.extend(card_color_sorted[color]) - return combined_cards - -def ColorCmc(deck): + + +def deck_metrics(deck): + """This function determines the total CMC, count, and distribution of a collection of cards""" + metrics = DeckMetrics() cmc_total = 0 - count = 0 - distribution = [0, 0, 0, 0, 0, 0, 0] - try: + + metrics.total_cards = len(deck) + for card in deck: - cmc_total += card[constants.DATA_FIELD_CMC] - count += 1 - index = int(min(card[constants.DATA_FIELD_CMC], len(distribution) - 1)) - distribution[index] += 1 - + if any(x in [constants.CARD_TYPE_CREATURE] + for x in card[constants.DATA_FIELD_TYPES]): + metrics.creature_count += 1 + metrics.total_non_land_cards += 1 + cmc_total += card[constants.DATA_FIELD_CMC] + + index = int( + min(card[constants.DATA_FIELD_CMC], + len(metrics.distribution_creatures) - 1)) + metrics.distribution_creatures[index] += 1 + else: + if constants.CARD_TYPE_LAND not in card[constants.DATA_FIELD_TYPES]: + cmc_total += card[constants.DATA_FIELD_CMC] + metrics.total_non_land_cards += 1 + index = int( + min(card[constants.DATA_FIELD_CMC], + len(metrics.distribution_noncreatures) - 1)) + metrics.distribution_noncreatures[index] += 1 + metrics.noncreature_count += 1 + + index = int( + min(card[constants.DATA_FIELD_CMC], + len(metrics.distribution_all) - 1)) + metrics.distribution_all[index] += 1 + + metrics.cmc_average = (cmc_total / metrics.total_non_land_cards + if metrics.total_non_land_cards + else 0.0) + except Exception as error: - logic_logger.info(f"ColorCmc Error: {error}") - - return cmc_total, count, distribution - -def OptionFilter(deck, option_selection, metrics, configuration): + logic_logger.info("deck_metrics error: %s", error) + + return metrics + + +def option_filter(deck, option_selection, metrics, configuration): + """This function returns a list of colors based on the deck filter option""" filtered_color_list = [option_selection] try: if constants.FILTER_OPTION_AUTO in option_selection: - filtered_color_list = AutoColors(deck, 2, metrics, configuration) + filtered_color_list = auto_colors(deck, 3, metrics, configuration) else: filtered_color_list = [option_selection] except Exception as error: - logic_logger.info(f"OptionFilter Error: {error}") + logic_logger.info("option_filter error: %s", error) return filtered_color_list - -def DeckColors(deck, colors_max, metrics, configuration): + + +def deck_colors(deck, colors_max, metrics, configuration): + """This function determines the prominent colors for a collection of cards""" + colors_result = {} try: - deck_colors = {} - - colors = CalculateColorAffinity(deck,constants.FILTER_OPTION_ALL_DECKS, metrics["mean"], configuration) - + threshold = metrics.mean - 0.33 * metrics.standard_deviation + colors = calculate_color_affinity( + deck, constants.FILTER_OPTION_ALL_DECKS, threshold, configuration) + # Modify the dictionary to include ratings - color_list = list(map((lambda x : {"color" : x, "rating" : colors[x]}), colors.keys())) - + color_list = list( + map((lambda x: {"color": x, "rating": colors[x]}), colors.keys())) + # Sort the list by decreasing ratings - color_list = sorted(color_list, key = lambda k : k["rating"], reverse = True) - + color_list = sorted( + color_list, key=lambda k: k["rating"], reverse=True) + # Remove extra colors beyond limit - color_list = color_list[0:3] - - # Return colors - sorted_colors = list(map((lambda x : x["color"]), color_list)) - - #Create color permutation + color_list = color_list[0:4] + + # Return colors + sorted_colors = list(map((lambda x: x["color"]), color_list)) + + # Create color permutation color_combination = [] - + for count in range(colors_max + 1): if count > 1: color_combination.extend(combinations(sorted_colors, count)) else: color_combination.extend((sorted_colors)) - #Convert tuples to list of strings + # Convert tuples to list of strings color_strings = [''.join(tups) for tups in color_combination] color_strings = [x for x in color_strings if len(x) <= colors_max] @@ -293,113 +461,149 @@ def DeckColors(deck, colors_max, metrics, configuration): color_dict = {} for color_string in color_strings: for color in color_string: - if color_string not in color_dict.keys(): + if color_string not in color_dict: color_dict[color_string] = 0 color_dict[color_string] += colors[color] - + for color_option in constants.DECK_COLORS: - for color_string in color_dict.keys(): - if (len(color_string) == len(color_option)) and set(color_string).issubset(color_option): - deck_colors[color_option] = color_dict[color_string] + for key, value in color_dict.items(): + if (len(key) == len(color_option)) and set(key).issubset(color_option): + colors_result[color_option] = value + + # Recalculate values based on the filtered win rates + for color in colors_result: + base_rating = calculate_color_rating(deck, + color, + threshold, + configuration) + curve_rating = calculate_curve_rating(deck, + color, + configuration) + colors_result[color] = curve_rating + base_rating + + # Add All Decks as a baseline + colors_result[constants.FILTER_OPTION_ALL_DECKS] = calculate_color_rating(deck, + constants.FILTER_OPTION_ALL_DECKS, + metrics.mean, + configuration) + colors_result = dict( + sorted(colors_result.items(), key=lambda item: item[1], reverse=True)) - deck_colors = dict(sorted(deck_colors.items(), key=lambda item: item[1], reverse=True)) - except Exception as error: - logic_logger.info(f"DeckColors Error: {error}") - - return deck_colors - -def AutoColors(deck, colors_max, metrics, configuration): + logic_logger.info("deck_colors error: %s", error) + + return colors_result + + +def auto_colors(deck, colors_max, metrics, configuration): + """When the Auto deck filter is selected, this function identifies the prominent color pairs from the collected cards""" try: deck_colors_list = [constants.FILTER_OPTION_ALL_DECKS] colors_dict = {} deck_length = len(deck) if deck_length > 15: - colors_dict = DeckColors(deck, colors_max, metrics, configuration) + colors_dict = deck_colors(deck, colors_max, metrics, configuration) colors = list(colors_dict.keys()) - auto_select_threshold = 30 - deck_length + auto_select_threshold = 80 - deck_length if (len(colors) > 1) and ((colors_dict[colors[0]] - colors_dict[colors[1]]) > auto_select_threshold): deck_colors_list = colors[0:1] elif len(colors) == 1: deck_colors_list = colors[0:1] - elif configuration.auto_highest_enabled == True: + elif configuration.auto_highest_enabled: deck_colors_list = colors[0:2] except Exception as error: - logic_logger.info(f"AutoColors Error: {error}") - + logic_logger.info("auto_colors error: %s", error) + return deck_colors_list -def CalculateColorAffinity(deck_cards, color_filter, threshold, configuration): - #Identify deck colors based on the number of high win rate cards + +def calculate_color_rating(cards, color_filter, threshold, configuration): + """This function identifies the main deck colors based on the GIHWR of the collected cards""" + rating = 0 + + for card in cards: + try: + gihwr = calculate_win_rate(card[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIHWR], + card[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIH], + configuration.bayesian_average_enabled) + if gihwr > threshold: + rating += gihwr - threshold + except Exception as error: + logic_logger.info("calculate_color_affinity error: %s", error) + return rating + + +def calculate_curve_rating(deck, color_filter, configuration): + """This function will assign a rating to a collection of cards based on how well they meet the deck building requirements""" + curve_rating_levels = [10, 10, 10, 10, 15, + 15, 15, 20, 20, 20, + 25, 25, 25, 30, 40, + 40, 40, 50, 50, 50] + + curve_start = 15 + pick_number = len(deck) + index = max(pick_number - curve_start, 0) + curve_rating = 0.0 + curve_rating_factor = 0.0 + minimum_creature_count = configuration.minimum_creatures + + try: + filtered_cards = deck_card_search( + deck, + color_filter, + constants.CARD_TYPE_DICT[constants.CARD_TYPE_SELECTION_NON_LANDS][0], + True, + True, + False) + deck_info = deck_metrics(filtered_cards) + curve_rating = curve_rating_levels[int( + min(index, len(curve_rating_levels) - 1))] + + if deck_info.total_cards < configuration.deck_mid.maximum_card_count: + curve_rating_factor -= ((configuration.deck_mid.maximum_card_count - deck_info.creature_count) + / configuration.deck_mid.maximum_card_count) * 1 + elif deck_info.creature_count < minimum_creature_count: + curve_rating_factor -= ((minimum_creature_count - + deck_info.creature_count) / minimum_creature_count) * 0.5 + elif deck_info.creature_count < configuration.deck_mid.recommended_creature_count: + curve_rating_factor += (deck_info.creature_count + / configuration.deck_mid.recommended_creature_count) * 0.5 + else: + curve_rating_factor += 0.5 + + if deck_info.cmc_average <= configuration.deck_mid.cmc_average: + curve_rating_factor += 0.5 + + except Exception as error: + logic_logger.info("calculate_curve_rating error: %s", error) + + return curve_rating * curve_rating_factor + + +def calculate_color_affinity(deck_cards, color_filter, threshold, configuration): + """This function identifies the main deck colors based on the GIHWR of the collected cards""" colors = {} - + for card in deck_cards: try: - gihwr = CalculateWinRate(card[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIHWR], - card[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIH], - configuration.bayesian_average_enabled) + gihwr = calculate_win_rate(card[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIHWR], + card[constants.DATA_FIELD_DECK_COLORS][color_filter][constants.DATA_FIELD_GIH], + configuration.bayesian_average_enabled) if gihwr > threshold: for color in card[constants.DATA_FIELD_COLORS]: if color not in colors: colors[color] = 0 colors[color] += (gihwr - threshold) except Exception as error: - logic_logger.info(f"CalculateColorAffinity Error: {error}") - return colors + logic_logger.info("calculate_color_affinity error: %s", error) + return colors -def CardFilter(card_list, deck, filtered_colors, fields, metrics, tier_list, configuration, curve_bonus, color_bonus): - filtered_list = [] - - deck_colors = DeckColors(deck, 2, metrics, configuration) - deck_colors = deck_colors.keys() - - for card in card_list: - try: - selected_card = card - selected_card["results"] = ["NA"] * len(fields) - selected_card["curve_bonus"] = [0.0] * len(filtered_colors) if curve_bonus else [] - selected_card["color_bonus"] = [0.0] * len(filtered_colors) if color_bonus else [] - - for count, option in enumerate(fields.values()): - if constants.FILTER_OPTION_TIER in option: - card_name = card[constants.DATA_FIELD_NAME].split(" // ") - if card_name[0] in tier_list[option][constants.DATA_SECTION_RATINGS]: - selected_card["results"][count] = tier_list[option][constants.DATA_SECTION_RATINGS][card_name[0]] - elif option == constants.DATA_FIELD_COLORS: - selected_card["results"][count] = "".join(card[option]) - elif option in card: - selected_card["results"][count] = card[option] - else: - rated_colors = [] - for color_index, color in enumerate(filtered_colors): - if (option in constants.WIN_RATE_OPTIONS) and (option in card[constants.DATA_FIELD_DECK_COLORS][color]): - rating_data = FormattedResult(card, - option, - constants.WIN_RATE_FIELDS_DICT[option], - metrics, - configuration, - color, - deck, - deck_colors, - curve_bonus, - color_bonus) - rated_colors.append(rating_data["result"]) - if "curve_bonus" in rating_data: - selected_card["curve_bonus"][color_index] = rating_data["curve_bonus"] - if "color_bonus" in rating_data: - selected_card["color_bonus"][color_index] = rating_data["color_bonus"] - elif option in card[constants.DATA_FIELD_DECK_COLORS][color]: - selected_card["results"][count] = card[constants.DATA_FIELD_DECK_COLORS][color][option] - if len(rated_colors): - selected_card["results"][count] = sorted(rated_colors, key=lambda x: FieldProcessSort(x), reverse = True)[0] - filtered_list.append(selected_card) - except Exception as error: - logic_logger.info(f"CardFilter Error: {error}") - return filtered_list +def row_color_tag(mana_cost): + """This function selects the color tag for a table row based on a card's mana cost""" + colors = list(card_colors(mana_cost).keys()) -def RowColorTag(colors): row_tag = constants.CARD_ROW_COLOR_GOLD_TAG if len(colors) > 1: row_tag = constants.CARD_ROW_COLOR_GOLD_TAG @@ -414,221 +618,140 @@ def RowColorTag(colors): elif constants.CARD_COLOR_SYMBOL_GREEN in colors: row_tag = constants.CARD_ROW_COLOR_GREEN_TAG return row_tag - -def CalculateMean(cards, bayesian_enabled): + + +def calculate_mean(cards, bayesian_enabled): + """The function calculates the mean win rate of a collection of cards""" card_count = 0 card_sum = 0 mean = 0 for card in cards: try: - winrate = CalculateWinRate(cards[card][constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_GIHWR], - cards[card][constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_GIH], - bayesian_enabled) - + winrate = calculate_win_rate(cards[card][constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_GIHWR], + cards[card][constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_GIH], + bayesian_enabled) + if winrate == 0: continue - + card_sum += winrate card_count += 1 - + except Exception as error: - logic_logger.info(f"CalculateMean Error: {error}") - + logic_logger.info("calculate_mean error: %s", error) + mean = float(card_sum / card_count) if card_count else 0 - + return mean - -def CalculateStandardDeviation(cards, mean, bayesian_enabled): + + +def calculate_standard_deviation(cards, mean, bayesian_enabled): + """The function calculates the standard deviation from the win rate of a collection of cards""" standard_deviation = 0 card_count = 0 sum_squares = 0 for card in cards: try: - winrate = CalculateWinRate(cards[card][constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_GIHWR], - cards[card][constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_GIH], - bayesian_enabled) - + winrate = calculate_win_rate(cards[card][constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_GIHWR], + cards[card][constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_GIH], + bayesian_enabled) + if winrate == 0: continue - + squared_deviations = (winrate - mean) ** 2 - + sum_squares += squared_deviations card_count += 1 - + except Exception as error: - logic_logger.info(f"CalculateStandardDeviation Error: {error}") - - #Find the variance + logic_logger.info("calculate_standard_deviation error: %s", error) + + # Find the variance variance = (sum_squares / (card_count - 1)) if card_count > 2 else 0 - + standard_deviation = math.sqrt(variance) - + return standard_deviation - -def RatingsLimits(cards, bayesian_enabled): + + +def ratings_limits(cards, bayesian_enabled): + """The function identifies the upper and lower win rates from a collection of cards""" upper_limit = 0 lower_limit = 100 - + for card in cards: for color in constants.DECK_COLORS: try: if color in cards[card][constants.DATA_FIELD_DECK_COLORS]: - gihwr = CalculateWinRate(cards[card][constants.DATA_FIELD_DECK_COLORS][color][constants.DATA_FIELD_GIHWR], - cards[card][constants.DATA_FIELD_DECK_COLORS][color][constants.DATA_FIELD_GIH], - bayesian_enabled) + gihwr = calculate_win_rate(cards[card][constants.DATA_FIELD_DECK_COLORS][color][constants.DATA_FIELD_GIHWR], + cards[card][constants.DATA_FIELD_DECK_COLORS][color][constants.DATA_FIELD_GIH], + bayesian_enabled) if gihwr > upper_limit: upper_limit = gihwr if gihwr < lower_limit and gihwr != 0: lower_limit = gihwr except Exception as error: - logic_logger.info(f"DeckRatingLimits Error: {error}") - + logic_logger.info("ratings_limits error: %s", error) + return upper_limit, lower_limit - -def CalculateWinRate(winrate, count, bayesian_enabled): + + +def calculate_win_rate(winrate, count, bayesian_enabled): + """The function will modify a card's win rate by applying the Bayesian Average algorithm or by zeroing a value with a low sample size""" calculated_winrate = 0.0 try: calculated_winrate = winrate - - if bayesian_enabled == True: + + if bayesian_enabled: win_count = winrate * count - calculated_winrate = (win_count + 1000)/ (count + 20) #Bayesian average calculation + # Bayesian average calculation + calculated_winrate = (win_count + 1000) / (count + 20) calculated_winrate = round(calculated_winrate, 2) else: + # Drop values that have fewer than 200 samples (same as 17Lands card_ratings page) if count < 200: calculated_winrate = 0.0 except Exception as error: - logic_logger.info(f"CalculateWinRate Error: {error}") + logic_logger.info("calculate_win_rate error: %s", error) return calculated_winrate - -def FormattedResult(card_data, winrate_field, winrate_count, metrics, configuration, filter, deck, deck_colors, enable_curve_bonus, enable_color_bonus): - rating_data = {"result" : 0} - #Produce a result that matches the Result Format setting - if configuration.result_format == constants.RESULT_FORMAT_RATING: - rating_data = CardRating(card_data, winrate_field, winrate_count, metrics, configuration, filter, deck, deck_colors, enable_curve_bonus, enable_color_bonus) - elif configuration.result_format == constants.RESULT_FORMAT_GRADE: - rating_data = CardGrade(card_data, winrate_field, winrate_count, metrics, configuration, filter) - else: - rating_data["result"] = CalculateWinRate(card_data[constants.DATA_FIELD_DECK_COLORS][filter][winrate_field], - card_data[constants.DATA_FIELD_DECK_COLORS][filter][winrate_count], - configuration.bayesian_average_enabled) - - return rating_data - -def CardGrade(card_data, winrate_field, winrate_count, metrics, configuration, filter): - rating_data = {"result" : constants.LETTER_GRADE_NA} - try: - winrate = CalculateWinRate(card_data[constants.DATA_FIELD_DECK_COLORS][filter][winrate_field], - card_data[constants.DATA_FIELD_DECK_COLORS][filter][winrate_count], - configuration.bayesian_average_enabled) - - if ((winrate != 0) and (metrics["standard_deviation"] != 0)): - rating_data["result"] = constants.LETTER_GRADE_F - for grade, deviation in constants.GRADE_DEVIATION_DICT.items(): - standard_score = (winrate - metrics["mean"]) / metrics["standard_deviation"] - if standard_score >= deviation: - rating_data["result"] = grade - break - - except Exception as error: - logic_logger.info(f"CardGrade Error: {error}") - return rating_data - -def CardRating(card_data, winrate_field, winrate_count, metrics, configuration, filter, deck, deck_colors, enable_curve_bonus, enable_color_bonus): - rating_data = {"result" : 0} - try: - winrate = CalculateWinRate(card_data[constants.DATA_FIELD_DECK_COLORS][filter][winrate_field], - card_data[constants.DATA_FIELD_DECK_COLORS][filter][winrate_count], - configuration.bayesian_average_enabled) - - upper_limit = metrics["mean"] + metrics["standard_deviation"] * 2 - lower_limit = metrics["mean"] - metrics["standard_deviation"] * 1.33 - - if (winrate != 0) and (upper_limit != lower_limit): - rating_data["result"] = round(((winrate - lower_limit) / (upper_limit - lower_limit)) * 5.0, 1) - rating_data["result"] = min(rating_data["result"], 5.0) - rating_data["result"] = max(rating_data["result"], 0) - - #upper_limit = 0 - #lower_limit = 0 - #if "upper" in metrics: - # upper_limit = metrics["upper"] - # - #if "lower" in metrics: - # lower_limit = metrics["lower"] -# - #if (enable_curve_bonus) and (filter != constants.FILTER_OPTION_ALL_DECKS): - # rating_data["curve_bonus"] = 0.0 -# - #if (enable_color_bonus) and (filter == constants.FILTER_OPTION_ALL_DECKS): - # rating_data["color_bonus"] = 0.0 - - #if (winrate != 0) and (upper_limit != lower_limit): - # #Curve bonus - # pick_number = len(deck) - # if "curve_bonus" in rating_data: - # rating_data["curve_bonus"] = CurveBonus(deck, card_data, pick_number, filter, configuration) - # - # #Color bonus - # if "color_bonus" in rating_data: - # rating_data["color_bonus"] = ColorBonus(deck, deck_colors, card_data, configuration.bayesian_average_enabled) - # - # #Calculate the ALSA bonus - # alsa_bonus = ((15 - card_data[constants.DATA_FIELD_DECK_COLORS][filter][constants.DATA_FIELD_ALSA]) / 10) * configuration.alsa_weight - # - # #Calculate IWD penalty - # iwd_penalty = 0 - # - # if card_data[constants.DATA_FIELD_DECK_COLORS][filter][constants.DATA_FIELD_IWD] < 0: - # iwd_penalty = (max(card_data[constants.DATA_FIELD_DECK_COLORS][filter][constants.DATA_FIELD_IWD], -10) / 10) * configuration.iwd_weight - # - # gihwr = min(gihwr, upper_limit) - # gihwr = max(gihwr, lower_limit) - # - # rating_data["result"] = ((gihwr - lower_limit) / (upper_limit - lower_limit)) * 5.0 - # - # #Make adjustments - # rating_data["result"] += alsa_bonus + iwd_penalty - # - # if "curve_bonus" in rating_data: - # rating_data["result"] += rating_data["curve_bonus"] - # - # if "color_bonus" in rating_data: - # rating_data["result"] += rating_data["color_bonus"] - # - # rating_data["result"] = round(rating_data["result"], 1) - # - # rating_data["result"] = min(rating_data["result"], 5.0) - # rating_data["result"] = max(rating_data["result"], 0) - - except Exception as error: - logic_logger.info(f"CardRating Error: {error}") - return rating_data - -def DeckColorStats(deck, color): + + +def deck_color_stats(deck, color): + """The function will identify the number of creature and noncreature cards in a collection of cards""" creature_count = 0 noncreature_count = 0 try: - creature_cards = DeckColorSearch(deck, color, constants.CARD_TYPE_DICT[constants.CARD_TYPE_SELECTION_CREATURES], True, True, False) - noncreature_cards = DeckColorSearch(deck, color, constants.CARD_TYPE_DICT[constants.CARD_TYPE_SELECTION_CREATURES], False, True, False) - + creature_cards = deck_card_search( + deck, color, [constants.CARD_TYPE_CREATURE], True, True, False) + noncreature_cards = deck_card_search( + deck, color, [constants.CARD_TYPE_CREATURE], False, True, False) + noncreature_cards = deck_card_search( + noncreature_cards, color, + [constants.CARD_TYPE_INSTANT, + constants.CARD_TYPE_SORCERY, + constants.CARD_TYPE_ARTIFACT, + constants.CARD_TYPE_ENCHANTMENT, + constants.CARD_TYPE_PLANESWALKER], True, True, False) + creature_count = len(creature_cards) noncreature_count = len(noncreature_cards) - + except Exception as error: - logic_logger.info(f"DeckColorStats Error: {error}") - + logic_logger.info("deck_color_stats error: %s", error) + return creature_count, noncreature_count - -def CardCmcSearch(deck, offset, starting_cmc, cmc_limit, remaining_count): + + +def card_cmc_search(deck, offset, starting_cmc, cmc_limit, remaining_count): + """The function will use recursion to search through a collection of cards and produce a list of cards with a mean CMC that is below a specific limit""" cards = [] unused = [] try: for count, card in enumerate(deck[offset:]): card_cmc = card[constants.DATA_FIELD_CMC] - + if card_cmc + starting_cmc <= cmc_limit: card_cmc += starting_cmc current_offset = offset + count @@ -642,367 +765,414 @@ def CardCmcSearch(deck, offset, starting_cmc, cmc_limit, remaining_count): break else: current_offset += 1 - cards, skipped = CardCmcSearch(deck, current_offset, card_cmc, cmc_limit, current_remaining) - if len(cards): + cards, skipped = card_cmc_search( + deck, current_offset, card_cmc, cmc_limit, current_remaining) + if cards: cards.append(card) unused.extend(skipped) - break + break else: - unused.append(card) - else: + unused.append(card) + else: unused.append(card) except Exception as error: - logic_logger.info(f"CardCmcSearch Error: {error}") - + logic_logger.info("card_cmc_search error: %s", error) + return cards, unused - -def DeckRating(deck, deck_type, color, threshold, bayesian_enabled): + + +def deck_rating(deck, deck_type, color, threshold, bayesian_enabled): + """The function will produce a deck rating based on the combined GIHWR value for each card with a GIHWR value above a certain threshold""" rating = 0 try: - #Combined GIHWR of the cards + # Combined GIHWR of the cards for card in deck: try: - gihwr = CalculateWinRate(card[constants.DATA_FIELD_DECK_COLORS][color][constants.DATA_FIELD_GIHWR], - card[constants.DATA_FIELD_DECK_COLORS][color][constants.DATA_FIELD_GIH], - bayesian_enabled) + gihwr = calculate_win_rate(card[constants.DATA_FIELD_DECK_COLORS][color][constants.DATA_FIELD_GIHWR], + card[constants.DATA_FIELD_DECK_COLORS][color][constants.DATA_FIELD_GIH], + bayesian_enabled) if gihwr > threshold: rating += gihwr - except Exception as error: + except Exception: pass - #Deck contains the recommended number of creatures + # Deck contains the recommended number of creatures recommended_creature_count = deck_type.recommended_creature_count - filtered_cards = DeckColorSearch(deck, color, constants.CARD_TYPE_DICT[constants.CARD_TYPE_SELECTION_CREATURES], True, True, False) - + filtered_cards = deck_card_search( + deck, color, [constants.CARD_TYPE_CREATURE], True, True, False) + if len(filtered_cards) < recommended_creature_count: rating -= (recommended_creature_count - len(filtered_cards)) * 100 - - #Average CMC of the creatures is below the ideal cmc average + + # Average CMC of the creatures is below the ideal cmc average cmc_average = deck_type.cmc_average total_cards = len(filtered_cards) total_cmc = 0 - + for card in filtered_cards: total_cmc += card[constants.DATA_FIELD_CMC] - + cmc = total_cmc / total_cards - + if cmc > cmc_average: rating -= 500 - - #Cards fit distribution + + # Cards fit distribution minimum_distribution = deck_type.distribution distribution = [0, 0, 0, 0, 0, 0, 0] for card in filtered_cards: - index = int(min(card[constants.DATA_FIELD_CMC], len(minimum_distribution) - 1)) + index = int(min(card[constants.DATA_FIELD_CMC], + len(minimum_distribution) - 1)) distribution[index] += 1 - + for index, value in enumerate(distribution): if value < minimum_distribution[index]: rating -= 100 - + except Exception as error: - logic_logger.info(f"DeckRating Error: {error}") - + logic_logger.info("deck_rating error: %s", error) + + rating = int(rating) + return rating - -def CopyDeck(deck, sideboard, set_cards): + + +def copy_deck(deck, sideboard): + """The function will produce a deck/sideboard list that is formatted in such a way that it can be copied to Arena and Sealdeck.tech""" deck_copy = "" - starting_index = 0 - total_deck = len(deck) - card_count = 0 - basic_lands = ["Mountain","Forest","Swamp","Plains","Island"] try: - #Copy Deck + # Copy Deck deck_copy = "Deck\n" - #identify the arena_id for the cards + # identify the arena_id for the cards for card in deck: - deck_copy += ("%d %s\n" % (card[constants.DATA_FIELD_COUNT],card[constants.DATA_FIELD_NAME])) - - #Copy sideboard - if sideboard != None: + deck_copy += f"{card[constants.DATA_FIELD_COUNT]} {card[constants.DATA_FIELD_NAME]}\n" + + # Copy sideboard + if sideboard is not None: deck_copy += "\nSideboard\n" for card in sideboard: - deck_copy += ("%d %s\n" % (card[constants.DATA_FIELD_COUNT],card[constants.DATA_FIELD_NAME])) - + deck_copy += f"{card[constants.DATA_FIELD_COUNT]} {card[constants.DATA_FIELD_NAME]}\n" + except Exception as error: - logic_logger.info(f"CopyDeck Error: {error}") - + logic_logger.info("copy_deck error: %s", error) + return deck_copy - - -def StackCards(cards): + + +def stack_cards(cards): + """The function will produce a list consisting of unique cards and the number of copies of each card""" deck = {} deck_list = [] for card in cards: try: name = card[constants.DATA_FIELD_NAME] - if name not in deck.keys(): - deck[name] = {constants.DATA_FIELD_COUNT : 1} - for field in constants.DATA_SET_FIELDS: - field = field - if field in card: - deck[name][field] = card[field] + if name not in deck: + deck[name] = {constants.DATA_FIELD_COUNT: 1} + for data_field in constants.DATA_SET_FIELDS: + if data_field in card: + deck[name][data_field] = card[data_field] else: deck[name][constants.DATA_FIELD_COUNT] += 1 except Exception as error: - logic_logger.info(f"StackCards Error: {error}") - #Convert to list format - for card in deck: - deck_list.append(deck[card]) + logic_logger.info("stack_cards error: %s", error) + # Convert to list format + deck_list = list(deck.values()) return deck_list - -def CardColors(mana_cost): - colors = [] + + +def card_colors(mana_cost): + """The function parses a mana cost string and returns a list of mana symbols""" + colors = {} try: - if constants.CARD_COLOR_SYMBOL_BLACK in mana_cost: - colors.append(constants.CARD_COLOR_SYMBOL_BLACK) - - if constants.CARD_COLOR_SYMBOL_GREEN in mana_cost: - colors.append(constants.CARD_COLOR_SYMBOL_GREEN) - - if constants.CARD_COLOR_SYMBOL_RED in mana_cost: - colors.append(constants.CARD_COLOR_SYMBOL_RED) - - if constants.CARD_COLOR_SYMBOL_BLUE in mana_cost: - colors.append(constants.CARD_COLOR_SYMBOL_BLUE) - - if constants.CARD_COLOR_SYMBOL_WHITE in mana_cost: - colors.append(constants.CARD_COLOR_SYMBOL_WHITE) + for color in constants.CARD_COLORS: + if color in mana_cost: + if color not in colors: + colors[color] = 1 + else: + colors[color] += 1 + except Exception as error: - print ("CardColors Error: %s" % error) + logic_logger.info("card_colors error: %s", error) return colors - -#Identify splashable color -def ColorSplash(cards, colors, splash_threshold, configuration): + + +def color_splash(cards, colors, splash_threshold, configuration): + """The function will parse a list of cards to determine if there are any cards that might justify a splash""" color_affinity = {} splash_color = "" try: - # Calculate affinity - color_affinity = CalculateColorAffinity(cards, colors, splash_threshold, configuration) - + # Calculate affinity to rank colors based on splash threshold (minimum GIHWR) + color_affinity = calculate_color_affinity( + cards, colors, splash_threshold, configuration) + # Modify the dictionary to include ratings - color_affinity = list(map((lambda x : {"color" : x, "rating" : color_affinity[x]}), color_affinity.keys())) - #Remove the current colors + color_affinity = list( + map((lambda x: {"color": x, "rating": color_affinity[x]}), color_affinity.keys())) + # Remove the current colors filtered_colors = color_affinity[:] for color in color_affinity: if color["color"] in colors: filtered_colors.remove(color) # Sort the list by decreasing ratings - filtered_colors = sorted(filtered_colors, key = lambda k : k["rating"], reverse = True) - - if len(filtered_colors): + filtered_colors = sorted( + filtered_colors, key=lambda k: k["rating"], reverse=True) + + if filtered_colors: splash_color = filtered_colors[0]["color"] except Exception as error: - logic_logger.info(f"ColorSplash Error: {error}") + logic_logger.info("color_splash error: %s", error) return splash_color - -#Identify the number of lands needed to fill the deck -def ManaBase(deck): + +def mana_base(deck): + """The function will identify the number of lands that are needed to fill out a deck""" maximum_deck_size = 40 combined_deck = [] - mana_types = {"Swamp" : {"color" : constants.CARD_COLOR_SYMBOL_BLACK, constants.DATA_FIELD_COUNT : 0}, - "Forest" : {"color" : constants.CARD_COLOR_SYMBOL_GREEN, constants.DATA_FIELD_COUNT : 0}, - "Mountain" : {"color" : constants.CARD_COLOR_SYMBOL_RED, constants.DATA_FIELD_COUNT : 0}, - "Island": {"color" : constants.CARD_COLOR_SYMBOL_BLUE, constants.DATA_FIELD_COUNT : 0}, - "Plains" : {"color" : constants.CARD_COLOR_SYMBOL_WHITE, constants.DATA_FIELD_COUNT : 0}} + mana_types = {"Swamp": {"color": constants.CARD_COLOR_SYMBOL_BLACK, constants.DATA_FIELD_COUNT: 0}, + "Forest": {"color": constants.CARD_COLOR_SYMBOL_GREEN, constants.DATA_FIELD_COUNT: 0}, + "Mountain": {"color": constants.CARD_COLOR_SYMBOL_RED, constants.DATA_FIELD_COUNT: 0}, + "Island": {"color": constants.CARD_COLOR_SYMBOL_BLUE, constants.DATA_FIELD_COUNT: 0}, + "Plains": {"color": constants.CARD_COLOR_SYMBOL_WHITE, constants.DATA_FIELD_COUNT: 0}} total_count = 0 try: - number_of_lands = 0 if maximum_deck_size < len(deck) else maximum_deck_size - len(deck) - - #Go through the cards and count the mana types + number_of_lands = 0 if maximum_deck_size < len( + deck) else maximum_deck_size - len(deck) + + # Go through the cards and count the mana types for card in deck: - mana_types["Swamp"][constants.DATA_FIELD_COUNT] += card["mana_cost"].count(constants.CARD_COLOR_SYMBOL_BLACK) - mana_types["Forest"][constants.DATA_FIELD_COUNT] += card["mana_cost"].count(constants.CARD_COLOR_SYMBOL_GREEN) - mana_types["Mountain"][constants.DATA_FIELD_COUNT] += card["mana_cost"].count(constants.CARD_COLOR_SYMBOL_RED) - mana_types["Island"][constants.DATA_FIELD_COUNT] += card["mana_cost"].count(constants.CARD_COLOR_SYMBOL_BLUE) - mana_types["Plains"][constants.DATA_FIELD_COUNT] += card["mana_cost"].count(constants.CARD_COLOR_SYMBOL_WHITE) - - for land in mana_types: - total_count += mana_types[land][constants.DATA_FIELD_COUNT] - - #Sort by lowest count - mana_types = dict(sorted(mana_types.items(), key=lambda t: t[1]['count'])) - #Add x lands with a distribution set by the mana types - for index, land in enumerate(mana_types): - if (mana_types[land][constants.DATA_FIELD_COUNT] == 1) and (number_of_lands > 1): - land_count = 1 - number_of_lands -= 1 + if constants.CARD_TYPE_LAND in card[constants.DATA_FIELD_TYPES]: + # Subtract symbol for lands + for mana_type in mana_types.values(): + mana_type[constants.DATA_FIELD_COUNT] -= (1 if (mana_type["color"] in card[constants.DATA_FIELD_COLORS]) + else 0) else: - land_count = round((mana_types[land][constants.DATA_FIELD_COUNT] / total_count) * number_of_lands, 0) - #Minimum of 2 lands for a splash - if (land_count == 1) and (number_of_lands > 1): - land_count = 2 - number_of_lands -= 1 - - if mana_types[land][constants.DATA_FIELD_COUNT] != 0: - card = {constants.DATA_FIELD_COLORS : mana_types[land]["color"], - constants.DATA_FIELD_TYPES : constants.CARD_TYPE_LAND, - constants.DATA_FIELD_CMC : 0, - constants.DATA_FIELD_NAME : land, - constants.DATA_FIELD_COUNT : land_count} - combined_deck.append(card) - + # Increase count for abilities that are not part of the mana cost + mana_count = card_colors(card[constants.DATA_FIELD_MANA_COST]) + # for color in card[constants.DATA_FIELD_COLORS]: + # mana_count[color] = ( + # mana_count[color] + 1) if color in mana_count else 1 + + for mana_type in mana_types.values(): + color = mana_type["color"] + mana_type[constants.DATA_FIELD_COUNT] += mana_count[color] if color in mana_count else 0 + + for land in mana_types.values(): + land[constants.DATA_FIELD_COUNT] = max( + land[constants.DATA_FIELD_COUNT], 0) + total_count += land[constants.DATA_FIELD_COUNT] + + # Sort by lowest count + mana_types = dict( + sorted(mana_types.items(), key=lambda t: t[1][constants.DATA_FIELD_COUNT])) + # Add x lands with a distribution set by the mana types + total_lands = number_of_lands + for land in mana_types: + if not total_lands or not mana_types[land][constants.DATA_FIELD_COUNT]: + continue + + land_count = int(math.ceil( + (mana_types[land][constants.DATA_FIELD_COUNT] / total_count) * number_of_lands)) + + land_count = min(land_count, total_lands) + total_lands -= land_count + + if land_count: + card = {constants.DATA_FIELD_COLORS: mana_types[land]["color"], + constants.DATA_FIELD_TYPES: constants.CARD_TYPE_LAND, + constants.DATA_FIELD_CMC: 0, + constants.DATA_FIELD_NAME: land, + constants.DATA_FIELD_MANA_COST: mana_types[land]["color"], + constants.DATA_FIELD_COUNT: land_count} + combined_deck.append(card) + except Exception as error: - logic_logger.info(f"ManaBase Error: {error}") + logic_logger.info("mana_base error: %s", error) return combined_deck - -def SuggestDeck(taken_cards, metrics, configuration): + + +def suggest_deck(taken_cards, metrics, configuration): + """The function will analyze the list of taken cards and produce several viable decks based on specific criteria""" colors_max = 3 - maximum_card_count = 23 + maximum_card_count = 22 sorted_decks = {} try: - deck_types = {"Mid" : configuration.deck_mid, "Aggro" : configuration.deck_aggro, "Control" :configuration.deck_control} - #Identify the top color combinations - colors = DeckColors(taken_cards, colors_max, metrics, configuration) - colors = colors.keys() + deck_types = {"Mid": configuration.deck_mid, + "Aggro": configuration.deck_aggro, + "Control": configuration.deck_control} + # Identify the top color combinations + colors = deck_colors(taken_cards, colors_max, metrics, configuration) filtered_colors = [] - - #Collect color stats and remove colors that don't meet the minimum requirements + + colors.pop(constants.FILTER_OPTION_ALL_DECKS, None) + + # Collect color stats and remove colors that don't meet the minimum requirements for color in colors: - creature_count, noncreature_count = DeckColorStats(taken_cards, color) - if((creature_count >= configuration.minimum_creatures) and + creature_count, noncreature_count = deck_color_stats( + taken_cards, color) + if ((creature_count >= configuration.minimum_creatures) and (noncreature_count >= configuration.minimum_noncreatures) and (creature_count + noncreature_count >= maximum_card_count)): filtered_colors.append(color) - + decks = {} + threshold = metrics.mean - 0.33 * metrics.standard_deviation for color in filtered_colors: - for type in deck_types.keys(): - deck, sideboard_cards = BuildDeck(deck_types[type], taken_cards, color, metrics, configuration) - rating = DeckRating(deck, deck_types[type], color, metrics["mean"], configuration.bayesian_average_enabled) + for key, value in deck_types.items(): + deck, sideboard_cards = build_deck( + value, taken_cards, color, metrics, configuration) + rating = deck_rating( + deck, value, color, threshold, configuration.bayesian_average_enabled) if rating >= configuration.ratings_threshold: - - if ((color not in decks.keys()) or - (color in decks.keys() and rating > decks[color]["rating"] )): + + if ((color not in decks) or + (color in decks and rating > decks[color]["rating"])): decks[color] = {} - decks[color]["deck_cards"] = StackCards(deck) - decks[color]["sideboard_cards"] = StackCards(sideboard_cards) + decks[color]["deck_cards"] = stack_cards(deck) + decks[color]["sideboard_cards"] = stack_cards( + sideboard_cards) decks[color]["rating"] = rating - decks[color]["type"] = type - decks[color]["deck_cards"].extend(ManaBase(deck)) - - sorted_colors = sorted(decks, key=lambda x: decks[x]["rating"], reverse=True) + decks[color]["type"] = key + decks[color]["deck_cards"].extend(mana_base(deck)) + + sorted_colors = sorted( + decks, key=lambda x: decks[x]["rating"], reverse=True) for color in sorted_colors: sorted_decks[color] = decks[color] except Exception as error: - logic_logger.info(f"SuggestDeck Error: {error}") + logic_logger.info("suggest_deck error: %s", error) return sorted_decks - -def BuildDeck(deck_type, cards, color, metrics, configuration): + + +def build_deck(deck_type, cards, color, metrics, configuration): + """The function will build a deck list that meets specific criteria""" minimum_distribution = deck_type.distribution maximum_card_count = deck_type.maximum_card_count maximum_deck_size = 40 cmc_average = deck_type.cmc_average recommended_creature_count = deck_type.recommended_creature_count - used_list = [] - sideboard_list = cards[:] #Copy by value + deck_list = [] + unused_creature_list = [] + sideboard_list = cards[:] # Copy by value try: for card in cards: - card["results"] = [CalculateWinRate(card[constants.DATA_FIELD_DECK_COLORS][color][constants.DATA_FIELD_GIHWR], - card[constants.DATA_FIELD_DECK_COLORS][color][constants.DATA_FIELD_GIH], - configuration.bayesian_average_enabled)] - - #identify a splashable color - splash_threshold = metrics["mean"] + 2.33 * metrics["standard_deviation"] - color +=(ColorSplash(cards, color, splash_threshold, configuration)) - - card_colors_sorted = DeckColorSearch(cards, color, constants.CARD_TYPE_DICT[constants.CARD_TYPE_SELECTION_CREATURES], True, True, False) - card_colors_sorted = sorted(card_colors_sorted, key = lambda k: k["results"][0], reverse = True) - - #Identify creatures that fit distribution - distribution = [0,0,0,0,0,0,0] - unused_list = [] - used_list = [] + card["results"] = [calculate_win_rate(card[constants.DATA_FIELD_DECK_COLORS][color][constants.DATA_FIELD_GIHWR], + card[constants.DATA_FIELD_DECK_COLORS][color][constants.DATA_FIELD_GIH], + configuration.bayesian_average_enabled)] + + # identify a splashable color + splash_threshold = metrics.mean + \ + 2.33 * metrics.standard_deviation + color += (color_splash(cards, color, splash_threshold, configuration)) + + card_colors_sorted = deck_card_search( + cards, color, [constants.CARD_TYPE_CREATURE], True, True, False) + card_colors_sorted = sorted( + card_colors_sorted, key=lambda k: k["results"][0], reverse=True) + + # Identify creatures that fit distribution + distribution = [0, 0, 0, 0, 0, 0, 0] used_count = 0 used_cmc_combined = 0 for card in card_colors_sorted: - index = int(min(card[constants.DATA_FIELD_CMC], len(minimum_distribution) - 1)) - if(distribution[index] < minimum_distribution[index]): - used_list.append(card) + index = int(min(card[constants.DATA_FIELD_CMC], + len(minimum_distribution) - 1)) + if distribution[index] < minimum_distribution[index]: + deck_list.append(card) + sideboard_list.remove(card) distribution[index] += 1 used_count += 1 used_cmc_combined += card[constants.DATA_FIELD_CMC] else: - unused_list.append(card) - - - #Go back and identify remaining creatures that have the highest base rating but don't push average above the threshold + unused_creature_list.append(card) + + # Go back and identify remaining creatures that have the highest base rating but don't push average above the threshold unused_cmc_combined = cmc_average * recommended_creature_count - used_cmc_combined - - unused_list.sort(key=lambda x : x["results"][0], reverse = True) - - #Identify remaining cards that won't exceed recommeneded CMC average - cmc_cards, unused_list = CardCmcSearch(unused_list, 0, 0, unused_cmc_combined, recommended_creature_count - used_count) - used_list.extend(cmc_cards) - - total_card_count = len(used_list) - - temp_unused_list = unused_list[:] + + unused_creature_list.sort(key=lambda x: x["results"][0], reverse=True) + + # Identify remaining cards that won't exceed recommeneded CMC average + cmc_cards, unused_creature_list = card_cmc_search( + unused_creature_list, 0, 0, unused_cmc_combined, recommended_creature_count - used_count) + + for card in cmc_cards: + deck_list.append(card) + sideboard_list.remove(card) + + total_card_count = len(deck_list) + if len(cmc_cards) == 0: - for card in unused_list: + for card in unused_creature_list: if total_card_count >= recommended_creature_count: break - - used_list.append(card) - temp_unused_list.remove(card) + + deck_list.append(card) + sideboard_list.remove(card) total_card_count += 1 - unused_list = temp_unused_list[:] - - card_colors_sorted = DeckColorSearch(cards, color, [constants.CARD_TYPE_INSTANT, constants.CARD_TYPE_SORCERY,constants.CARD_TYPE_ENCHANTMENT,constants.CARD_TYPE_ARTIFACT], True, True, False) - card_colors_sorted = sorted(card_colors_sorted, key = lambda k: k["results"][0], reverse = True) - #Add non-creature cards + + card_colors_sorted = deck_card_search(sideboard_list, color, [ + constants.CARD_TYPE_INSTANT, + constants.CARD_TYPE_SORCERY, + constants.CARD_TYPE_ENCHANTMENT, + constants.CARD_TYPE_ARTIFACT, + constants.CARD_TYPE_PLANESWALKER], True, True, False) + card_colors_sorted = sorted( + card_colors_sorted, key=lambda k: k["results"][0], reverse=True) + + # Add instant, sorcery, enchantment, etc for card in card_colors_sorted: if total_card_count >= maximum_card_count: break - - used_list.append(card) + + deck_list.append(card) + sideboard_list.remove(card) total_card_count += 1 - - - #Fill the deck with remaining creatures - for card in unused_list: + + card_colors_sorted = deck_card_search(sideboard_list, color, [ + constants.CARD_TYPE_CREATURE], True, True, False) + card_colors_sorted = sorted( + card_colors_sorted, key=lambda k: k["results"][0], reverse=True) + + # Fill the deck with the remaining creature cards + for card in card_colors_sorted: if total_card_count >= maximum_card_count: break - - used_list.append(card) + + deck_list.append(card) + sideboard_list.remove(card) total_card_count += 1 - - #Add in special lands if they are on-color, off-color, and they have a card rating above 2.0 - land_cards = DeckColorSearch(cards, color, [constants.CARD_TYPE_LAND], True, True, False) - land_cards = [x for x in land_cards if x[constants.DATA_FIELD_NAME] not in constants.BASIC_LANDS] - land_cards = sorted(land_cards, key = lambda k: k["results"][0], reverse = True) + # Add in special lands if they have a win rate that is at least 0.33 standard deviations from the mean (C-) + land_cards = deck_card_search( + sideboard_list, color, [constants.CARD_TYPE_LAND], True, True, False) + land_cards = [ + x for x in land_cards if x[constants.DATA_FIELD_NAME] not in constants.BASIC_LANDS] + land_cards = sorted( + land_cards, key=lambda k: k["results"][0], reverse=True) for card in land_cards: if total_card_count >= maximum_deck_size: break - - if card["results"][0] >= metrics["mean"]: - used_list.append(card) - total_card_count += 1 - - - #Identify sideboard cards: - for card in used_list: - try: + + if card["results"][0] >= metrics.mean - 0.33 * metrics.standard_deviation: + deck_list.append(card) sideboard_list.remove(card) - except Exception as error: - print("%s error: %s" % (card[constants.DATA_FIELD_NAME], error)) - logic_logger.info(f"Sideboard {card['name']} Error: {error}") + total_card_count += 1 + except Exception as error: - logic_logger.info(f"BuildDeck Error: {error}") - return used_list, sideboard_list - -def ReadConfig(): + logic_logger.info("build_deck error: %s", error) + return deck_list, sideboard_list + + +def read_config(): + """The function will retrieve settings values from a configuration file""" config = Config() try: - with open("config.json", 'r') as data: + with open("config.json", 'r', encoding="utf8", errors="replace") as data: config_json = data.read() config_data = json.loads(config_json) config.hotkey_enabled = config_data["features"]["hotkey_enabled"] config.images_enabled = config_data["features"]["images_enabled"] + config.scale_factor = config_data["features"]["scale_factor"] config.database_size = config_data["card_data"]["database_size"] config.table_width = int(config_data["settings"]["table_width"]) config.deck_filter = config_data["settings"]["deck_filter"] @@ -1024,22 +1194,26 @@ def ReadConfig(): config.taken_gpwr_enabled = config_data["settings"]["taken_gpwr_enabled"] config.taken_ohwr_enabled = config_data["settings"]["taken_ohwr_enabled"] config.taken_iwd_enabled = config_data["settings"]["taken_iwd_enabled"] + config.taken_gdwr_enabled = config_data["settings"]["taken_gdwr_enabled"] config.taken_gndwr_enabled = config_data["settings"]["taken_gndwr_enabled"] config.card_colors_enabled = config_data["settings"]["card_colors_enabled"] config.bayesian_average_enabled = config_data["settings"]["bayesian_average_enabled"] config.draft_log_enabled = config_data["settings"]["draft_log_enabled"] + config.color_identity_enabled = config_data["settings"]["color_identity_enabled"] except Exception as error: - logic_logger.info(f"ReadConfig Error: {error}") + logic_logger.info("read_config error: %s", error) return config -def WriteConfig(config): + +def write_config(config): + """The function will write configuration values to a configuration file""" try: - with open("config.json", 'r') as data: + with open("config.json", 'r', encoding="utf8", errors="replace") as data: config_json = data.read() config_data = json.loads(config_json) - + config_data["card_data"]["database_size"] = config.database_size - + config_data["settings"]["column_2"] = config.column_2 config_data["settings"]["column_3"] = config.column_3 config_data["settings"]["column_4"] = config.column_4 @@ -1058,31 +1232,36 @@ def WriteConfig(config): config_data["settings"]["taken_ata_enabled"] = config.taken_ata_enabled config_data["settings"]["taken_gpwr_enabled"] = config.taken_gpwr_enabled config_data["settings"]["taken_ohwr_enabled"] = config.taken_ohwr_enabled + config_data["settings"]["taken_gdwr_enabled"] = config.taken_gdwr_enabled config_data["settings"]["taken_gndwr_enabled"] = config.taken_gndwr_enabled config_data["settings"]["taken_iwd_enabled"] = config.taken_iwd_enabled config_data["settings"]["card_colors_enabled"] = config.card_colors_enabled config_data["settings"]["bayesian_average_enabled"] = config.bayesian_average_enabled config_data["settings"]["draft_log_enabled"] = config.draft_log_enabled - - with open('config.json', 'w', encoding='utf-8') as file: + config_data["settings"]["color_identity_enabled"] = config.color_identity_enabled + + with open("config.json", 'w', encoding="utf-8", errors="replace") as file: json.dump(config_data, file, ensure_ascii=False, indent=4) - + except Exception as error: - logic_logger.info(f"WriteConfig Error: {error}") + logic_logger.info("write_config error: %s", error) -def ResetConfig(): + +def reset_config(): + """The function will reset the application's configuration values back to the hard-coded default values""" config = Config() data = {} - + try: - + data["features"] = {} data["features"]["hotkey_enabled"] = config.hotkey_enabled data["features"]["images_enabled"] = config.images_enabled - + data["features"]["scale_factor"] = config.scale_factor + data["card_data"] = {} data["card_data"]["database_size"] = config.database_size - + data["settings"] = {} data["settings"]["table_width"] = config.table_width data["settings"]["column_2"] = config.column_2 @@ -1105,10 +1284,12 @@ def ResetConfig(): data["settings"]["taken_ata_enabled"] = config.taken_ata_enabled data["settings"]["taken_gpwr_enabled"] = config.taken_gpwr_enabled data["settings"]["taken_ohwr_enabled"] = config.taken_ohwr_enabled + data["settings"]["taken_gdwr_enabled"] = config.taken_gdwr_enabled data["settings"]["taken_gndwr_enabled"] = config.taken_gndwr_enabled data["settings"]["taken_iwd_enabled"] = config.taken_iwd_enabled data["settings"]["card_colors_enabled"] = config.card_colors_enabled - + data["settings"]["color_identity_enabled"] = config.color_identity_enabled + data["card_logic"] = {} data["card_logic"]["alsa_weight"] = config.alsa_weight data["card_logic"]["iwd_weight"] = config.iwd_weight @@ -1121,9 +1302,10 @@ def ResetConfig(): data["card_logic"]["deck_types"]["Aggro"] = {} data["card_logic"]["deck_types"]["Aggro"] = asdict(config.deck_aggro) data["card_logic"]["deck_types"]["Control"] = {} - data["card_logic"]["deck_types"]["Control"] = asdict(config.deck_control) - - with open('config.json', 'w', encoding='utf-8') as file: + data["card_logic"]["deck_types"]["Control"] = asdict( + config.deck_control) + + with open("config.json", 'w', encoding="utf-8", errors="replace") as file: json.dump(data, file, ensure_ascii=False, indent=4) except Exception as error: - logic_logger.info(f"ResetConfig Error: {error}") \ No newline at end of file + logic_logger.info("reset_config error: %s", error) diff --git a/config.json b/config.json index 84a874a..ad12f42 100644 --- a/config.json +++ b/config.json @@ -1,7 +1,8 @@ { "features": { "hotkey_enabled": true, - "images_enabled": true + "images_enabled": true, + "scale_factor": 1.0 }, "card_data": { "database_size": 0 @@ -28,9 +29,11 @@ "taken_ata_enabled": false, "taken_gpwr_enabled": false, "taken_ohwr_enabled": false, + "taken_gdwr_enabled": false, "taken_gndwr_enabled": false, "taken_iwd_enabled": false, - "card_colors_enabled": false + "card_colors_enabled": false, + "color_identity_enabled": false }, "card_logic": { "alsa_weight": 0.0, diff --git a/constants.py b/constants.py index 8f4d23e..a30537a 100644 --- a/constants.py +++ b/constants.py @@ -1,7 +1,9 @@ import os import getpass -# Global Constants -## The different types of draft. + +TKINTER_DEFAULT_DPI = 72 + +DEFAULT_RESOLUTION_WIDTH = 1920 FONT_SANS_SERIF = "Arial" FONT_MONO_SPACE = "Courier" @@ -11,45 +13,56 @@ HOTKEY_CTRL_G = '\x07' -BASIC_LANDS = ["Island","Mountain","Swamp","Plains","Forest"] +BASIC_LANDS = ["Island", "Mountain", "Swamp", "Plains", "Forest"] CARD_COLOR_SYMBOL_WHITE = "W" CARD_COLOR_SYMBOL_BLACK = "B" -CARD_COLOR_SYMBOL_BLUE = "U" -CARD_COLOR_SYMBOL_RED = "R" +CARD_COLOR_SYMBOL_BLUE = "U" +CARD_COLOR_SYMBOL_RED = "R" CARD_COLOR_SYMBOL_GREEN = "G" -CARD_COLOR_SYMBOL_NONE = "NC" - -CARD_COLOR_LABEL_WHITE = "White" -CARD_COLOR_LABEL_BLACK = "Black" -CARD_COLOR_LABEL_BLUE = "Blue" -CARD_COLOR_LABEL_RED = "Red" -CARD_COLOR_LABEL_GREEN = "Green" - -LIMITED_TYPE_UNKNOWN = 0 -LIMITED_TYPE_DRAFT_PREMIER_V1 = 1 -LIMITED_TYPE_DRAFT_PREMIER_V2 = 2 -LIMITED_TYPE_DRAFT_QUICK = 3 -LIMITED_TYPE_DRAFT_TRADITIONAL = 4 -LIMITED_TYPE_SEALED = 5 +CARD_COLOR_SYMBOL_NONE = "NC" + +CARD_COLORS = [ + CARD_COLOR_SYMBOL_WHITE, + CARD_COLOR_SYMBOL_BLACK, + CARD_COLOR_SYMBOL_BLUE, + CARD_COLOR_SYMBOL_RED, + CARD_COLOR_SYMBOL_GREEN +] + +CARD_COLOR_LABEL_WHITE = "White" +CARD_COLOR_LABEL_BLACK = "Black" +CARD_COLOR_LABEL_BLUE = "Blue" +CARD_COLOR_LABEL_RED = "Red" +CARD_COLOR_LABEL_GREEN = "Green" +CARD_COLOR_LABEL_NC = "NC" + +LIMITED_TYPE_UNKNOWN = 0 +LIMITED_TYPE_DRAFT_PREMIER_V1 = 1 +LIMITED_TYPE_DRAFT_PREMIER_V2 = 2 +LIMITED_TYPE_DRAFT_QUICK = 3 +LIMITED_TYPE_DRAFT_TRADITIONAL = 4 +LIMITED_TYPE_SEALED = 5 LIMITED_TYPE_SEALED_TRADITIONAL = 6 URL_17LANDS = "https://www.17lands.com" IMAGE_17LANDS_SITE_PREFIX = "/static/images/" -DATA_FIELD_17LANDS_OHWR = "opening_hand_win_rate" -DATA_FIELD_17LANDS_NGOH = "opening_hand_game_count" -DATA_FIELD_17LANDS_GPWR = "win_rate" -DATA_FIELD_17LANDS_NGP = "game_count" -DATA_FIELD_17LANDS_GIHWR = "ever_drawn_win_rate" -DATA_FIELD_17LANDS_IWD = "drawn_improvement_win_rate" -DATA_FIELD_17LANDS_ALSA = "avg_seen" -DATA_FIELD_17LANDS_GIH = "ever_drawn_game_count" -DATA_FIELD_17LANDS_ATA = "avg_pick" -DATA_FIELD_17LANDS_NGND = "never_drawn_game_count" -DATA_FIELD_17LANDS_GNDWR = "never_drawn_win_rate" -DATA_FIELD_17LANDS_IMAGE = "url" +DATA_FIELD_17LANDS_OHWR = "opening_hand_win_rate" +DATA_FIELD_17LANDS_NGOH = "opening_hand_game_count" +DATA_FIELD_17LANDS_GPWR = "win_rate" +DATA_FIELD_17LANDS_NGP = "game_count" +DATA_FIELD_17LANDS_GIHWR = "ever_drawn_win_rate" +DATA_FIELD_17LANDS_IWD = "drawn_improvement_win_rate" +DATA_FIELD_17LANDS_ALSA = "avg_seen" +DATA_FIELD_17LANDS_GIH = "ever_drawn_game_count" +DATA_FIELD_17LANDS_ATA = "avg_pick" +DATA_FIELD_17LANDS_NGND = "never_drawn_game_count" +DATA_FIELD_17LANDS_GNDWR = "never_drawn_win_rate" +DATA_FIELD_17LANDS_GDWR = "drawn_win_rate" +DATA_FIELD_17LANDS_NGD = "drawn_game_count" +DATA_FIELD_17LANDS_IMAGE = "url" DATA_FIELD_17LANDS_IMAGE_BACK = "url_back" @@ -64,6 +77,9 @@ DATA_FIELD_GIH = "gih" DATA_FIELD_GNDWR = "gndwr" DATA_FIELD_NGND = "ngnd" +DATA_FIELD_GDWR = "gdwr" +DATA_FIELD_NGD = "ngd" +DATA_FIELD_WHEEL = "wheel" DATA_SECTION_IMAGES = "image" DATA_SECTION_RATINGS = "ratings" @@ -75,28 +91,33 @@ DATA_FIELD_DECK_COLORS = "deck_colors" DATA_FIELD_COUNT = "count" DATA_FIELD_DISABLED = "disabled" +DATA_FIELD_RARITY = "rarity" +DATA_FIELD_MANA_COST = "mana_cost" -DATA_FIELDS_LIST = [DATA_FIELD_GIHWR, - DATA_FIELD_OHWR, - DATA_FIELD_GPWR, +DATA_FIELDS_LIST = [DATA_FIELD_GIHWR, + DATA_FIELD_OHWR, + DATA_FIELD_GPWR, DATA_FIELD_GNDWR, DATA_FIELD_ALSA, - DATA_FIELD_ATA, + DATA_FIELD_ATA, DATA_FIELD_IWD, DATA_FIELD_NGP, DATA_FIELD_NGOH, DATA_FIELD_GIH, - DATA_FIELD_NGND] - -DATA_SET_FIELDS = [DATA_FIELD_GIHWR, - DATA_FIELD_OHWR, - DATA_FIELD_GPWR, - DATA_FIELD_ALSA, - DATA_FIELD_IWD, - DATA_FIELD_CMC, - DATA_FIELD_COLORS, - DATA_FIELD_NAME, + DATA_FIELD_NGND, + DATA_FIELD_GDWR, + DATA_FIELD_NGD] + +DATA_SET_FIELDS = [DATA_FIELD_GIHWR, + DATA_FIELD_OHWR, + DATA_FIELD_GPWR, + DATA_FIELD_ALSA, + DATA_FIELD_IWD, + DATA_FIELD_CMC, + DATA_FIELD_COLORS, + DATA_FIELD_NAME, DATA_FIELD_TYPES, + DATA_FIELD_MANA_COST, DATA_SECTION_IMAGES, DATA_FIELD_DECK_COLORS] @@ -104,27 +125,37 @@ FILTER_OPTION_AUTO = "Auto" FILTER_OPTION_TIER = "Tier" -#FIELD_LABEL_ATA = "Average Taken At (ATA)" -#FIELD_LABEL_ALSA = "Average Last Seen At (ALSA)" -#FIELD_LABEL_IWD = "Improvement When Drawn (IWD)" -#FIELD_LABEL_OHWR = "Opening Hand Win Rate (OHWR)" -#FIELD_LABEL_GPWR = "Games Played Win Rate (GPWR)" -#FIELD_LABEL_GIHWR = "Games In Hand Win Rate (GIHWR)" -FIELD_LABEL_ATA = "ATA" -FIELD_LABEL_ALSA = "ALSA" -FIELD_LABEL_IWD = "IWD" -FIELD_LABEL_OHWR = "OHWR" -FIELD_LABEL_GPWR = "GPWR" -FIELD_LABEL_GIHWR = "GIHWR" -FIELD_LABEL_DISABLED = "DISABLED" -FIELD_LABEL_COLORS = "COLORS" -FIELD_LABEL_GNDWR = "GNDWR" +FIELD_LABEL_ATA = "ATA: Average Taken At" +FIELD_LABEL_ALSA = "ALSA: Average Last Seen At" +FIELD_LABEL_IWD = "IWD: Improvement When Drawn" +FIELD_LABEL_OHWR = "OHWR: Opening Hand Win Rate" +FIELD_LABEL_GPWR = "GPWR: Games Played Win Rate" +FIELD_LABEL_GIHWR = "GIHWR: Games In Hand Win Rate" +FIELD_LABEL_GNDWR = "GNDWR: Games Not Drawn Win Rate" +FIELD_LABEL_COLORS = "COLORS: Card Colors" +FIELD_LABEL_DISABLED = "DISABLED: Remove Column" +FIELD_LABEL_COUNT = "COUNT: Total Card Count" +FIELD_LABEL_WHEEL = "WHEEL: Probability of Wheeling" +FIELD_LABEL_GDWR = "GDWR: Games Drawn Win Rate" +#FIELD_LABEL_ATA = "ATA" +#FIELD_LABEL_ALSA = "ALSA" +#FIELD_LABEL_IWD = "IWD" +#FIELD_LABEL_OHWR = "OHWR" +#FIELD_LABEL_GPWR = "GPWR" +#FIELD_LABEL_GIHWR = "GIHWR" +#FIELD_LABEL_DISABLED = "DISABLED" +#FIELD_LABEL_COLORS = "COLORS" +#FIELD_LABEL_GNDWR = "GNDWR" +#FIELD_LABEL_COUNT = "COUNT" DATA_SET_VERSION_3 = 3.0 -WIN_RATE_OPTIONS = [DATA_FIELD_GIHWR, DATA_FIELD_OHWR, DATA_FIELD_GPWR, DATA_FIELD_GNDWR] -NON_COLORS_OPTIONS = WIN_RATE_OPTIONS + [DATA_FIELD_IWD, DATA_FIELD_ALSA, DATA_FIELD_ATA] -DECK_COLORS = [FILTER_OPTION_ALL_DECKS,CARD_COLOR_SYMBOL_WHITE,CARD_COLOR_SYMBOL_BLUE,CARD_COLOR_SYMBOL_BLACK,CARD_COLOR_SYMBOL_RED,CARD_COLOR_SYMBOL_GREEN,"WU","WB","WR","WG","UB","UR","UG","BR","BG","RG","WUB","WUR","WUG","WBR","WBG","WRG","UBR","UBG","URG","BRG"] +WIN_RATE_OPTIONS = [DATA_FIELD_GIHWR, DATA_FIELD_OHWR, + DATA_FIELD_GPWR, DATA_FIELD_GNDWR, DATA_FIELD_GDWR] +NON_COLORS_OPTIONS = WIN_RATE_OPTIONS + \ + [DATA_FIELD_IWD, DATA_FIELD_ALSA, DATA_FIELD_ATA] +DECK_COLORS = [FILTER_OPTION_ALL_DECKS, CARD_COLOR_SYMBOL_WHITE, CARD_COLOR_SYMBOL_BLUE, CARD_COLOR_SYMBOL_BLACK, CARD_COLOR_SYMBOL_RED, + CARD_COLOR_SYMBOL_GREEN, "WU", "WB", "WR", "WG", "UB", "UR", "UG", "BR", "BG", "RG", "WUB", "WUR", "WUG", "WBR", "WBG", "WRG", "UBR", "UBG", "URG", "BRG"] COLUMN_OPTIONS = NON_COLORS_OPTIONS DECK_FILTERS = [FILTER_OPTION_AUTO] + DECK_COLORS @@ -147,9 +178,10 @@ DRAFT_START_STRING_EVENT_JOIN = "[UnityCrossThreadLogger]==> Event_Join " DRAFT_START_STRING_BOT_DRAFT = "[UnityCrossThreadLogger]==> BotDraft_DraftStatus " -DRAFT_START_STRINGS = [DRAFT_START_STRING_EVENT_JOIN, DRAFT_START_STRING_BOT_DRAFT] +DRAFT_START_STRINGS = [DRAFT_START_STRING_EVENT_JOIN, + DRAFT_START_STRING_BOT_DRAFT] -DATA_SOURCES_NONE = {"None" : ""} +DATA_SOURCES_NONE = {"None": ""} DECK_FILTER_FORMAT_NAMES = "Names" DECK_FILTER_FORMAT_COLORS = "Colors" @@ -161,12 +193,15 @@ RESULT_FORMAT_RATING = "Rating" RESULT_FORMAT_GRADE = "Grade" -RESULT_FORMAT_LIST = [RESULT_FORMAT_WIN_RATE, RESULT_FORMAT_RATING, RESULT_FORMAT_GRADE] +RESULT_FORMAT_LIST = [RESULT_FORMAT_WIN_RATE, + RESULT_FORMAT_RATING, RESULT_FORMAT_GRADE] -LOCAL_DATA_FOLDER_PATH_WINDOWS = os.path.join("Wizards of the Coast","MTGA","MTGA_Data") -LOCAL_DATA_FOLDER_PATH_OSX = os.path.join("Library","Application Support","com.wizards.mtga") +LOCAL_DATA_FOLDER_PATH_WINDOWS = os.path.join( + "Wizards of the Coast", "MTGA", "MTGA_Data") +LOCAL_DATA_FOLDER_PATH_OSX = os.path.join( + "Library", "Application Support", "com.wizards.mtga") -LOCAL_DOWNLOADS_DATA = os.path.join("Downloads","Raw") +LOCAL_DOWNLOADS_DATA = os.path.join("Downloads", "Raw") LOCAL_DATA_FILE_PREFIX_CARDS = "Raw_cards_" LOCAL_DATA_FILE_PREFIX_DATABASE = "Raw_CardDatabase_" @@ -197,7 +232,7 @@ GROUP BY {LOCAL_DATABASE_LOCALIZATION_COLUMN_ID}) B ON A.{LOCAL_DATABASE_LOCALIZATION_COLUMN_ID} = B.{LOCAL_DATABASE_LOCALIZATION_COLUMN_ID} AND A.{LOCAL_DATABASE_LOCALIZATION_COLUMN_FORMAT} = B.MIN_FORMAT""" - + LOCAL_DATABASE_ENUMERATOR_QUERY = f"""SELECT {LOCAL_DATABASE_ENUMERATOR_COLUMN_ID}, {LOCAL_DATABASE_ENUMERATOR_COLUMN_TYPE}, @@ -217,6 +252,7 @@ LOCAL_CARDS_KEY_CMC = "cmc" LOCAL_CARDS_KEY_COLOR_ID = "coloridentity" LOCAL_CARDS_KEY_CASTING_COST = "castingcost" +LOCAL_CARDS_KEY_RARITY = "rarity" SETS_FOLDER = os.path.join(os.getcwd(), "Sets") SET_FILE_SUFFIX = "Data.json" @@ -231,13 +267,15 @@ PLATFORM_ID_OSX = "darwin" PLATFORM_ID_WINDOWS = "win32" -LOG_LOCATION_WINDOWS = os.path.join('Users', getpass.getuser(), "AppData", "LocalLow","Wizards Of The Coast","MTGA","Player.log") -LOG_LOCATION_OSX = os.path.join("Library","Logs","Wizards of the Coast","MTGA","Player.log") +LOG_LOCATION_WINDOWS = os.path.join('Users', getpass.getuser( +), "AppData", "LocalLow", "Wizards Of The Coast", "MTGA", "Player.log") +LOG_LOCATION_OSX = os.path.join( + "Library", "Logs", "Wizards of the Coast", "MTGA", "Player.log") DEFAULT_GIHWR_AVERAGE = 0.0 -WINDOWS_DRIVES = ["C:/","D:/","E:/","F:/"] -WINDOWS_PROGRAM_FILES = ["Program Files","Program Files (x86)"] +WINDOWS_DRIVES = ["C:/", "D:/", "E:/", "F:/"] +WINDOWS_PROGRAM_FILES = ["Program Files", "Program Files (x86)"] LIMITED_TYPE_STRING_DRAFT_PREMIER = "PremierDraft" LIMITED_TYPE_STRING_DRAFT_QUICK = "QuickDraft" @@ -247,7 +285,7 @@ LIMITED_TYPE_STRING_TRAD_SEALED = "TradSealed" LIMITED_TYPE_LIST = [ - LIMITED_TYPE_STRING_DRAFT_PREMIER, + LIMITED_TYPE_STRING_DRAFT_PREMIER, LIMITED_TYPE_STRING_DRAFT_QUICK, LIMITED_TYPE_STRING_DRAFT_TRAD, LIMITED_TYPE_STRING_SEALED, @@ -269,7 +307,7 @@ SET_SELECTION_CUBE = "CUBE" SET_RELEASE_OFFSET_DAYS = -7 -SET_LIST_COUNT_MAX = 24 +SET_LIST_COUNT_MAX = 24 SET_ARENA_CUBE_START_OFFSET_DAYS = -45 @@ -293,207 +331,249 @@ CARD_ROW_COLOR_GREEN_TAG = "green_card" CARD_ROW_COLOR_GOLD_TAG = "gold_card" -LETTER_GRADE_A_PLUS = "A+" -LETTER_GRADE_A = "A " +LETTER_GRADE_A_PLUS = "A+" +LETTER_GRADE_A = "A " LETTER_GRADE_A_MINUS = "A-" -LETTER_GRADE_B_PLUS = "B+" -LETTER_GRADE_B = "B " +LETTER_GRADE_B_PLUS = "B+" +LETTER_GRADE_B = "B " LETTER_GRADE_B_MINUS = "B-" -LETTER_GRADE_C_PLUS = "C+" -LETTER_GRADE_C = "C " +LETTER_GRADE_C_PLUS = "C+" +LETTER_GRADE_C = "C " LETTER_GRADE_C_MINUS = "C-" -LETTER_GRADE_D_PLUS = "D+" -LETTER_GRADE_D = "D " +LETTER_GRADE_D_PLUS = "D+" +LETTER_GRADE_D = "D " LETTER_GRADE_D_MINUS = "D-" -LETTER_GRADE_F = "F " -LETTER_GRADE_NA = "NA" +LETTER_GRADE_F = "F " +LETTER_GRADE_NA = "NA" -CARD_TYPE_CREATURE = "Creature" +CARD_TYPE_CREATURE = "Creature" CARD_TYPE_PLANESWALKER = "Planeswalker" -CARD_TYPE_INSTANT = "Instant" -CARD_TYPE_SORCERY = "Sorcery" -CARD_TYPE_ENCHANTMENT = "Enchantment" -CARD_TYPE_ARTIFACT = "Artifact" -CARD_TYPE_LAND = "Land" +CARD_TYPE_INSTANT = "Instant" +CARD_TYPE_SORCERY = "Sorcery" +CARD_TYPE_ENCHANTMENT = "Enchantment" +CARD_TYPE_ARTIFACT = "Artifact" +CARD_TYPE_LAND = "Land" -CARD_TYPE_SELECTION_ALL = "All" +CARD_TYPE_SELECTION_ALL = "All Cards" CARD_TYPE_SELECTION_CREATURES = "Creatures" CARD_TYPE_SELECTION_NONCREATURES = "Noncreatures" - -#Dictionaries -## Used to identify the limited type based on log string +CARD_TYPE_SELECTION_NON_LANDS = "Non-Lands" + +TABLE_MISSING = "missing" +TABLE_PACK = "pack" +TABLE_COMPARE = "compare" +TABLE_TAKEN = "taken" +TABLE_SUGGEST = "suggest" +TABLE_STATS = "stats" +TABLE_SETS = "sets" + +CARD_RARITY_COMMON = "common" +CARD_RARITY_UNCOMMON = "uncommon" +CARD_RARITY_RARE = "rare" +CARD_RARITY_MYTHIC = "mythic" + +# Dictionaries +# Used to identify the limited type based on log string LIMITED_TYPES_DICT = { - LIMITED_TYPE_STRING_DRAFT_PREMIER : LIMITED_TYPE_DRAFT_PREMIER_V1, - LIMITED_TYPE_STRING_DRAFT_QUICK : LIMITED_TYPE_DRAFT_QUICK, - LIMITED_TYPE_STRING_DRAFT_TRAD : LIMITED_TYPE_DRAFT_TRADITIONAL, - LIMITED_TYPE_STRING_DRAFT_BOT : LIMITED_TYPE_DRAFT_QUICK, - LIMITED_TYPE_STRING_SEALED : LIMITED_TYPE_SEALED, - LIMITED_TYPE_STRING_TRAD_SEALED : LIMITED_TYPE_SEALED_TRADITIONAL, + LIMITED_TYPE_STRING_DRAFT_PREMIER: LIMITED_TYPE_DRAFT_PREMIER_V1, + LIMITED_TYPE_STRING_DRAFT_QUICK: LIMITED_TYPE_DRAFT_QUICK, + LIMITED_TYPE_STRING_DRAFT_TRAD: LIMITED_TYPE_DRAFT_TRADITIONAL, + LIMITED_TYPE_STRING_DRAFT_BOT: LIMITED_TYPE_DRAFT_QUICK, + LIMITED_TYPE_STRING_SEALED: LIMITED_TYPE_SEALED, + LIMITED_TYPE_STRING_TRAD_SEALED: LIMITED_TYPE_SEALED_TRADITIONAL, } COLOR_NAMES_DICT = { - CARD_COLOR_SYMBOL_WHITE : CARD_COLOR_LABEL_WHITE, - CARD_COLOR_SYMBOL_BLUE : CARD_COLOR_LABEL_BLUE, - CARD_COLOR_SYMBOL_BLACK : CARD_COLOR_LABEL_BLACK, - CARD_COLOR_SYMBOL_RED : CARD_COLOR_LABEL_RED, - CARD_COLOR_SYMBOL_GREEN : CARD_COLOR_LABEL_GREEN, - "WU" : "Azorius", - "UB" : "Dimir", - "BR" : "Rakdos", - "RG" : "Gruul", - "WG" : "Selesnya", - "WB" : "Orzhov", - "BG" : "Golgari", - "UG" : "Simic", - "UR" : "Izzet", - "WR" : "Boros", - "WUR" : "Jeskai", - "UBG" : "Sultai", - "WBR" : "Mardu", - "URG" : "Temur", - "WBG" : "Abzan", - "WUB" : "Esper", - "UBR" : "Grixis", - "BRG" : "Jund", - "WRG" : "Naya", - "WUG" : "Bant", + CARD_COLOR_SYMBOL_WHITE: CARD_COLOR_LABEL_WHITE, + CARD_COLOR_SYMBOL_BLUE: CARD_COLOR_LABEL_BLUE, + CARD_COLOR_SYMBOL_BLACK: CARD_COLOR_LABEL_BLACK, + CARD_COLOR_SYMBOL_RED: CARD_COLOR_LABEL_RED, + CARD_COLOR_SYMBOL_GREEN: CARD_COLOR_LABEL_GREEN, + "WU": "Azorius", + "UB": "Dimir", + "BR": "Rakdos", + "RG": "Gruul", + "WG": "Selesnya", + "WB": "Orzhov", + "BG": "Golgari", + "UG": "Simic", + "UR": "Izzet", + "WR": "Boros", + "WUR": "Jeskai", + "UBG": "Sultai", + "WBR": "Mardu", + "URG": "Temur", + "WBG": "Abzan", + "WUB": "Esper", + "UBR": "Grixis", + "BRG": "Jund", + "WRG": "Naya", + "WUG": "Bant", } CARD_COLORS_DICT = { - CARD_COLOR_LABEL_WHITE : CARD_COLOR_SYMBOL_WHITE, - CARD_COLOR_LABEL_BLACK : CARD_COLOR_SYMBOL_BLACK, - CARD_COLOR_LABEL_BLUE : CARD_COLOR_SYMBOL_BLUE, - CARD_COLOR_LABEL_RED : CARD_COLOR_SYMBOL_RED, - CARD_COLOR_LABEL_GREEN : CARD_COLOR_SYMBOL_GREEN, + CARD_COLOR_LABEL_WHITE: CARD_COLOR_SYMBOL_WHITE, + CARD_COLOR_LABEL_BLACK: CARD_COLOR_SYMBOL_BLACK, + CARD_COLOR_LABEL_BLUE: CARD_COLOR_SYMBOL_BLUE, + CARD_COLOR_LABEL_RED: CARD_COLOR_SYMBOL_RED, + CARD_COLOR_LABEL_GREEN: CARD_COLOR_SYMBOL_GREEN, + CARD_COLOR_LABEL_NC: "", } PLATFORM_LOG_DICT = { - PLATFORM_ID_OSX : LOG_LOCATION_OSX, - PLATFORM_ID_WINDOWS : LOG_LOCATION_WINDOWS, + PLATFORM_ID_OSX: LOG_LOCATION_OSX, + PLATFORM_ID_WINDOWS: LOG_LOCATION_WINDOWS, } WIN_RATE_FIELDS_DICT = { - DATA_FIELD_GIHWR : DATA_FIELD_GIH, - DATA_FIELD_OHWR : DATA_FIELD_NGOH, - DATA_FIELD_GPWR : DATA_FIELD_NGP, - DATA_FIELD_GNDWR : DATA_FIELD_NGND, + DATA_FIELD_GIHWR: DATA_FIELD_GIH, + DATA_FIELD_OHWR: DATA_FIELD_NGOH, + DATA_FIELD_GPWR: DATA_FIELD_NGP, + DATA_FIELD_GNDWR: DATA_FIELD_NGND, + DATA_FIELD_GDWR: DATA_FIELD_NGD, } DATA_FIELD_17LANDS_DICT = { - DATA_FIELD_GIHWR : DATA_FIELD_17LANDS_GIHWR, - DATA_FIELD_OHWR : DATA_FIELD_17LANDS_OHWR, - DATA_FIELD_GPWR : DATA_FIELD_17LANDS_GPWR, - DATA_FIELD_ALSA : DATA_FIELD_17LANDS_ALSA, - DATA_FIELD_IWD : DATA_FIELD_17LANDS_IWD, - DATA_FIELD_ATA : DATA_FIELD_17LANDS_ATA, - DATA_FIELD_NGP : DATA_FIELD_17LANDS_NGP, - DATA_FIELD_NGOH : DATA_FIELD_17LANDS_NGOH, - DATA_FIELD_GIH : DATA_FIELD_17LANDS_GIH, - DATA_FIELD_GNDWR : DATA_FIELD_17LANDS_GNDWR, - DATA_FIELD_NGND : DATA_FIELD_17LANDS_NGND, - DATA_SECTION_IMAGES : [DATA_FIELD_17LANDS_IMAGE, DATA_FIELD_17LANDS_IMAGE_BACK] + DATA_FIELD_GIHWR: DATA_FIELD_17LANDS_GIHWR, + DATA_FIELD_OHWR: DATA_FIELD_17LANDS_OHWR, + DATA_FIELD_GPWR: DATA_FIELD_17LANDS_GPWR, + DATA_FIELD_ALSA: DATA_FIELD_17LANDS_ALSA, + DATA_FIELD_IWD: DATA_FIELD_17LANDS_IWD, + DATA_FIELD_ATA: DATA_FIELD_17LANDS_ATA, + DATA_FIELD_NGP: DATA_FIELD_17LANDS_NGP, + DATA_FIELD_NGOH: DATA_FIELD_17LANDS_NGOH, + DATA_FIELD_GIH: DATA_FIELD_17LANDS_GIH, + DATA_FIELD_GNDWR: DATA_FIELD_17LANDS_GNDWR, + DATA_FIELD_NGND: DATA_FIELD_17LANDS_NGND, + DATA_FIELD_GDWR: DATA_FIELD_17LANDS_GDWR, + DATA_FIELD_NGD: DATA_FIELD_17LANDS_NGD, + DATA_SECTION_IMAGES: [DATA_FIELD_17LANDS_IMAGE, + DATA_FIELD_17LANDS_IMAGE_BACK] } COLUMNS_OPTIONS_MAIN_DICT = { - FIELD_LABEL_ATA : DATA_FIELD_ATA, - FIELD_LABEL_ALSA : DATA_FIELD_ALSA, - FIELD_LABEL_IWD : DATA_FIELD_IWD, - FIELD_LABEL_OHWR : DATA_FIELD_OHWR, - FIELD_LABEL_GPWR : DATA_FIELD_GPWR, - FIELD_LABEL_GIHWR : DATA_FIELD_GIHWR, - FIELD_LABEL_GNDWR : DATA_FIELD_GNDWR, - FIELD_LABEL_COLORS : DATA_FIELD_COLORS, + FIELD_LABEL_ATA: DATA_FIELD_ATA, + FIELD_LABEL_ALSA: DATA_FIELD_ALSA, + FIELD_LABEL_IWD: DATA_FIELD_IWD, + FIELD_LABEL_OHWR: DATA_FIELD_OHWR, + FIELD_LABEL_GPWR: DATA_FIELD_GPWR, + FIELD_LABEL_GIHWR: DATA_FIELD_GIHWR, + FIELD_LABEL_GDWR: DATA_FIELD_GDWR, + FIELD_LABEL_GNDWR: DATA_FIELD_GNDWR, + FIELD_LABEL_COLORS: DATA_FIELD_COLORS, } COLUMNS_OPTIONS_EXTRA_DICT = { - FIELD_LABEL_DISABLED : DATA_FIELD_DISABLED, - FIELD_LABEL_ATA : DATA_FIELD_ATA, - FIELD_LABEL_ALSA : DATA_FIELD_ALSA, - FIELD_LABEL_IWD : DATA_FIELD_IWD, - FIELD_LABEL_OHWR : DATA_FIELD_OHWR, - FIELD_LABEL_GPWR : DATA_FIELD_GPWR, - FIELD_LABEL_GIHWR : DATA_FIELD_GIHWR, - FIELD_LABEL_GNDWR : DATA_FIELD_GNDWR, - FIELD_LABEL_COLORS : DATA_FIELD_COLORS, + FIELD_LABEL_DISABLED: DATA_FIELD_DISABLED, + FIELD_LABEL_ATA: DATA_FIELD_ATA, + FIELD_LABEL_ALSA: DATA_FIELD_ALSA, + FIELD_LABEL_IWD: DATA_FIELD_IWD, + FIELD_LABEL_OHWR: DATA_FIELD_OHWR, + FIELD_LABEL_GPWR: DATA_FIELD_GPWR, + FIELD_LABEL_GIHWR: DATA_FIELD_GIHWR, + FIELD_LABEL_GDWR: DATA_FIELD_GDWR, + FIELD_LABEL_GNDWR: DATA_FIELD_GNDWR, + FIELD_LABEL_COLORS: DATA_FIELD_COLORS, } -STATS_HEADER_CONFIG = {"Colors" : {"width" : .19, "anchor" : "w"}, - "1" : {"width" : .11, "anchor" : "c"}, - "2" : {"width" : .11, "anchor" : "c"}, - "3" : {"width" : .11, "anchor" : "c"}, - "4" : {"width" : .11, "anchor" : "c"}, - "5" : {"width" : .11, "anchor" : "c"}, - "6+" : {"width" : .11, "anchor" : "c"}, - "Total" : {"width" : .15, "anchor" : "c"}} - +STATS_HEADER_CONFIG = {"Colors": {"width": .19, "anchor": "w"}, + "1": {"width": .11, "anchor": "c"}, + "2": {"width": .11, "anchor": "c"}, + "3": {"width": .11, "anchor": "c"}, + "4": {"width": .11, "anchor": "c"}, + "5": {"width": .11, "anchor": "c"}, + "6+": {"width": .11, "anchor": "c"}, + "Total": {"width": .15, "anchor": "c"}} + ROW_TAGS_BW_DICT = { - BW_ROW_COLOR_ODD_TAG : (FONT_SANS_SERIF, "#3d3d3d", "#e6ecec"), - BW_ROW_COLOR_EVEN_TAG : (FONT_SANS_SERIF, "#333333", "#e6ecec"), + BW_ROW_COLOR_ODD_TAG: (FONT_SANS_SERIF, "#3d3d3d", "#e6ecec"), + BW_ROW_COLOR_EVEN_TAG: (FONT_SANS_SERIF, "#333333", "#e6ecec"), } ROW_TAGS_COLORS_DICT = { - CARD_ROW_COLOR_WHITE_TAG : (FONT_SANS_SERIF, "#E9E9E9", "#000000"), - CARD_ROW_COLOR_RED_TAG : (FONT_SANS_SERIF, "#FF6C6C", "#000000"), - CARD_ROW_COLOR_BLUE_TAG : (FONT_SANS_SERIF, "#6078F3", "#000000"), - CARD_ROW_COLOR_BLACK_TAG : (FONT_SANS_SERIF, "#BFBFBF", "#000000"), - CARD_ROW_COLOR_GREEN_TAG : (FONT_SANS_SERIF, "#60DC68", "#000000"), - CARD_ROW_COLOR_GOLD_TAG : (FONT_SANS_SERIF, "#F0E657", "#000000"), + CARD_ROW_COLOR_WHITE_TAG: (FONT_SANS_SERIF, "#E9E9E9", "#000000"), + CARD_ROW_COLOR_RED_TAG: (FONT_SANS_SERIF, "#FF6C6C", "#000000"), + CARD_ROW_COLOR_BLUE_TAG: (FONT_SANS_SERIF, "#6078F3", "#000000"), + CARD_ROW_COLOR_BLACK_TAG: (FONT_SANS_SERIF, "#BFBFBF", "#000000"), + CARD_ROW_COLOR_GREEN_TAG: (FONT_SANS_SERIF, "#60DC68", "#000000"), + CARD_ROW_COLOR_GOLD_TAG: (FONT_SANS_SERIF, "#F0E657", "#000000"), } GRADE_ORDER_DICT = { - LETTER_GRADE_A_PLUS : 13, - LETTER_GRADE_A : 12, - LETTER_GRADE_A_MINUS : 11, - LETTER_GRADE_B_PLUS : 10, - LETTER_GRADE_B : 9, - LETTER_GRADE_B_MINUS : 8, - LETTER_GRADE_C_PLUS : 7, - LETTER_GRADE_C : 6, - LETTER_GRADE_C_MINUS : 5, - LETTER_GRADE_D_PLUS : 4, - LETTER_GRADE_D : 3, - LETTER_GRADE_D_MINUS : 2, - LETTER_GRADE_F : 1, - LETTER_GRADE_NA : 0 + LETTER_GRADE_A_PLUS: 13, + LETTER_GRADE_A: 12, + LETTER_GRADE_A_MINUS: 11, + LETTER_GRADE_B_PLUS: 10, + LETTER_GRADE_B: 9, + LETTER_GRADE_B_MINUS: 8, + LETTER_GRADE_C_PLUS: 7, + LETTER_GRADE_C: 6, + LETTER_GRADE_C_MINUS: 5, + LETTER_GRADE_D_PLUS: 4, + LETTER_GRADE_D: 3, + LETTER_GRADE_D_MINUS: 2, + LETTER_GRADE_F: 1, + LETTER_GRADE_NA: 0 } TIER_CONVERSION_RATINGS_GRADES_DICT = { - LETTER_GRADE_A_PLUS : 5.0, - LETTER_GRADE_A : 4.6, - LETTER_GRADE_A_MINUS : 4.2, - LETTER_GRADE_B_PLUS : 3.8, - LETTER_GRADE_B : 3.5, - LETTER_GRADE_B_MINUS : 3.1, - LETTER_GRADE_C_PLUS : 2.7, - LETTER_GRADE_C : 2.3, - LETTER_GRADE_C_MINUS : 1.9, - LETTER_GRADE_D_PLUS : 1.5, - LETTER_GRADE_D : 1.2, - LETTER_GRADE_D_MINUS : 0.8 + LETTER_GRADE_A_PLUS: 5.0, + LETTER_GRADE_A: 4.6, + LETTER_GRADE_A_MINUS: 4.2, + LETTER_GRADE_B_PLUS: 3.8, + LETTER_GRADE_B: 3.5, + LETTER_GRADE_B_MINUS: 3.1, + LETTER_GRADE_C_PLUS: 2.7, + LETTER_GRADE_C: 2.3, + LETTER_GRADE_C_MINUS: 1.9, + LETTER_GRADE_D_PLUS: 1.5, + LETTER_GRADE_D: 1.2, + LETTER_GRADE_D_MINUS: 0.8, + LETTER_GRADE_F: 0.4 } GRADE_DEVIATION_DICT = { - LETTER_GRADE_A_PLUS : 2.33, - LETTER_GRADE_A : 2, - LETTER_GRADE_A_MINUS : 1.67, - LETTER_GRADE_B_PLUS : 1.33, - LETTER_GRADE_B : 1, - LETTER_GRADE_B_MINUS : 0.67, - LETTER_GRADE_C_PLUS : 0.33, - LETTER_GRADE_C : 0, - LETTER_GRADE_C_MINUS : -0.33, - LETTER_GRADE_D_PLUS : -0.67, - LETTER_GRADE_D : -1, - LETTER_GRADE_D_MINUS : -1.33 + LETTER_GRADE_A_PLUS: 2.33, + LETTER_GRADE_A: 2, + LETTER_GRADE_A_MINUS: 1.67, + LETTER_GRADE_B_PLUS: 1.33, + LETTER_GRADE_B: 1, + LETTER_GRADE_B_MINUS: 0.67, + LETTER_GRADE_C_PLUS: 0.33, + LETTER_GRADE_C: 0, + LETTER_GRADE_C_MINUS: -0.33, + LETTER_GRADE_D_PLUS: -0.67, + LETTER_GRADE_D: -1, + LETTER_GRADE_D_MINUS: -1.33 } - + CARD_TYPE_DICT = { - CARD_TYPE_SELECTION_ALL : [CARD_TYPE_CREATURE, CARD_TYPE_PLANESWALKER, CARD_TYPE_INSTANT, CARD_TYPE_SORCERY, CARD_TYPE_ENCHANTMENT, CARD_TYPE_ARTIFACT, CARD_TYPE_LAND], - CARD_TYPE_SELECTION_CREATURES : [CARD_TYPE_CREATURE, CARD_TYPE_PLANESWALKER], - CARD_TYPE_SELECTION_NONCREATURES : [CARD_TYPE_INSTANT, CARD_TYPE_SORCERY, CARD_TYPE_ENCHANTMENT, CARD_TYPE_ARTIFACT, CARD_TYPE_LAND], - CARD_TYPE_INSTANT : [CARD_TYPE_INSTANT], - CARD_TYPE_SORCERY : [CARD_TYPE_SORCERY], - CARD_TYPE_ENCHANTMENT : [CARD_TYPE_ENCHANTMENT], - CARD_TYPE_ARTIFACT : [CARD_TYPE_ARTIFACT], - CARD_TYPE_LAND : [CARD_TYPE_LAND] -} \ No newline at end of file + CARD_TYPE_SELECTION_ALL: ([CARD_TYPE_CREATURE, CARD_TYPE_PLANESWALKER, CARD_TYPE_INSTANT, CARD_TYPE_SORCERY, CARD_TYPE_ENCHANTMENT, CARD_TYPE_ARTIFACT, CARD_TYPE_LAND], True, False, True), + CARD_TYPE_SELECTION_CREATURES: ([CARD_TYPE_CREATURE], True, False, True), + CARD_TYPE_SELECTION_NONCREATURES: ([CARD_TYPE_CREATURE], False, False, True), + CARD_TYPE_SELECTION_NON_LANDS: ([CARD_TYPE_CREATURE, CARD_TYPE_PLANESWALKER, CARD_TYPE_INSTANT, CARD_TYPE_SORCERY, CARD_TYPE_ENCHANTMENT, CARD_TYPE_ARTIFACT], True, False, True), +} + +TABLE_PROPORTIONS = [ + (1,), + (.75, .25), + (.60, .20, .20), + (.46, .18, .18, .18) +] + +WHEEL_COEFFICIENTS = [ + [-0.46, 7.97, -27.43, 26.61], + [-0.33, 6.31, -23.12, 23.86], + [-0.19, 4.39, -17.06, 17.71], + [-0.06, 2.27, -9.22, 9.43], + [0.08, 0.15, -1.88, 2.36], + [0.25, -2.65, 9.76, -11.21], +] + +CARD_RARITY_DICT = { + 1: CARD_RARITY_COMMON, + 2: CARD_RARITY_COMMON, + 3: CARD_RARITY_UNCOMMON, + 4: CARD_RARITY_RARE, + 5: CARD_RARITY_MYTHIC, +} diff --git a/dark_mode.tcl b/dark_mode.tcl index e3c00c3..bfa0607 100644 --- a/dark_mode.tcl +++ b/dark_mode.tcl @@ -21,7 +21,6 @@ array set colors { -insertcolor $colors(-fg) \ -insertwidth 1 \ -fieldbackground $colors(-selectbg) \ - -font {"Arial" 10} \ -borderwidth 1 \ -relief flat @@ -35,5 +34,4 @@ array set colors { ttk::style map . -foreground [list disabled $colors(-disabledfg)] - option add *font [ttk::style lookup . -font] option add *Menu.selectcolor $colors(-fg) diff --git a/file_extractor.py b/file_extractor.py index 754ac25..63c8f7c 100644 --- a/file_extractor.py +++ b/file_extractor.py @@ -1,3 +1,6 @@ +"""This module contains the functions and classes that are used for building the set files and communicating with platforms""" +from enum import Enum +from urllib.parse import quote as urlencode import sys import os import time @@ -10,9 +13,7 @@ import re import sqlite3 import constants -from enum import Enum -import log_scanner as LS -from urllib.parse import quote as urlencode + if not os.path.exists(constants.SETS_FOLDER): os.makedirs(constants.SETS_FOLDER) @@ -25,17 +26,21 @@ file_logger = logging.getLogger(constants.LOG_TYPE_DEBUG) + class Result(Enum): + '''Enumeration class for file integrity results''' VALID = 0 ERROR_MISSING_FILE = 1 ERROR_UNREADABLE_FILE = 2 - -def DecodeManaCost(encoded_cost): + + +def decode_mana_cost(encoded_cost): + '''Parse the raw card mana_cost field and return the cards cmc and color identity list''' decoded_cost = "" cmc = 0 if len(encoded_cost): cost_string = re.sub('\(|\)', '', encoded_cost) - + sections = cost_string[1:].split("o") index = 0 for count, section in enumerate(sections): @@ -45,11 +50,13 @@ def DecodeManaCost(encoded_cost): index += 1 cmc += int(section) if section.isnumeric() else 1 - decoded_cost = "".join("{{{0}}}".format(x) for x in sections[0:index]) - + decoded_cost = "".join(f"{{{x}}}" for x in sections[0:index]) + return decoded_cost, cmc - -def RetrieveLocalSetList(sets): + + +def retrieve_local_set_list(sets): + '''Scans the Sets folder and returns a list of valid set files''' file_list = [] main_sets = [v[constants.SET_LIST_17LANDS][0] for k, v in sets.items()] for file in os.listdir(constants.SETS_FOLDER): @@ -57,100 +64,117 @@ def RetrieveLocalSetList(sets): name_segments = file.split("_") if len(name_segments) == 3: - if ((name_segments[0].upper() in main_sets) and - (name_segments[1] in constants.LIMITED_TYPES_DICT.keys()) and - (name_segments[2] == constants.SET_FILE_SUFFIX)): - - set_name = list(sets.keys())[list(main_sets).index(name_segments[0].upper())] - result, json_data = FileIntegrityCheck(os.path.join(constants.SETS_FOLDER,file)) + if ((name_segments[0].upper() in main_sets) and + (name_segments[1] in constants.LIMITED_TYPES_DICT) and + (name_segments[2] == constants.SET_FILE_SUFFIX)): + + set_name = list(sets.keys())[list( + main_sets).index(name_segments[0].upper())] + result, json_data = check_file_integrity( + os.path.join(constants.SETS_FOLDER, file)) if result == Result.VALID: if json_data["meta"]["version"] == 1: - start_date, end_date = json_data["meta"]["date_range"].split("->") + start_date, end_date = json_data["meta"]["date_range"].split( + "->") else: - start_date = json_data["meta"]["start_date"] - end_date = json_data["meta"]["end_date"] - file_list.append((set_name, name_segments[1], start_date, end_date)) + start_date = json_data["meta"]["start_date"] + end_date = json_data["meta"]["end_date"] + file_list.append( + (set_name, name_segments[1], start_date, end_date)) except Exception as error: - file_logger.info(f"RetrieveLocalSetList Error: {error}") - + file_logger.info("retrieve_local_set_list Error: %s", error) + return file_list - -def ArenaLogLocation(): + + +def retrieve_arena_log_location(): + '''Searches local directories for the location of the Arena Player.log file''' log_location = "" try: if sys.platform == constants.PLATFORM_ID_OSX: - paths = [os.path.join(os.path.expanduser('~'), constants.LOG_LOCATION_OSX)] + paths = [os.path.join(os.path.expanduser( + '~'), constants.LOG_LOCATION_OSX)] else: - path_list = [constants.WINDOWS_DRIVES, [constants.LOG_LOCATION_WINDOWS]] - paths = [os.path.join(*x) for x in itertools.product(*path_list)] - + path_list = [constants.WINDOWS_DRIVES, + [constants.LOG_LOCATION_WINDOWS]] + paths = [os.path.join(*x) for x in itertools.product(*path_list)] + for file_path in paths: - file_logger.info(f"Arena Log: Searching file path {file_path}") + file_logger.info("Arena Log: Searching file path %s", file_path) if os.path.exists(file_path): log_location = file_path break - + except Exception as error: - file_logger.info(f"ArenaLogLocation Error: {error}") + file_logger.info("retrieve_arena_log_location Error: %s", error) return log_location - -def ArenaDirectoryLocation(log_location): + + +def retrieve_arena_directory(log_location): + '''Searches the Player.log file for the Arena install location (windows only)''' arena_directory = "" try: - #Retrieve the arena directory - with open(log_location, 'r') as log_file: + # Retrieve the arena directory + with open(log_location, 'r', encoding="utf-8", errors="replace") as log_file: line = log_file.readline() location = re.findall(r"'(.*?)/Managed'", line, re.DOTALL) if location and os.path.exists(location[0]): arena_directory = location[0] - + except Exception as error: - file_logger.info(f"ArenaDirectoryLocation Error: {error}") + file_logger.info("retrieve_arena_directory Error: %s", error) return arena_directory - -def SearchLocalFiles(paths, file_prefixes): + + +def search_local_files(paths, file_prefixes): + '''Generic function that's used for searching local directories for a file''' file_locations = [] for file_path in paths: - try: + try: if os.path.exists(file_path): for prefix in file_prefixes: - files = [filename for filename in os.listdir(file_path) if filename.startswith(prefix)] - + files = [filename for filename in os.listdir( + file_path) if filename.startswith(prefix)] + for file in files: file_location = os.path.join(file_path, file) file_locations.append(file_location) - + except Exception as error: - file_logger.info(f"SearchLocalFiles Error: {error}") - + file_logger.info("search_local_files Error: %s", error) + return file_locations -def ExtractTypes(type_line): + +def extract_types(type_line): + '''Parses a type string and returns a list of card types''' types = [] if constants.CARD_TYPE_CREATURE in type_line: types.append(constants.CARD_TYPE_CREATURE) - + if constants.CARD_TYPE_PLANESWALKER in type_line: types.append(constants.CARD_TYPE_PLANESWALKER) - + if constants.CARD_TYPE_LAND in type_line: types.append(constants.CARD_TYPE_LAND) - + if constants.CARD_TYPE_INSTANT in type_line: types.append(constants.CARD_TYPE_INSTANT) - + if constants.CARD_TYPE_SORCERY in type_line: types.append(constants.CARD_TYPE_SORCERY) - + if constants.CARD_TYPE_ENCHANTMENT in type_line: types.append(constants.CARD_TYPE_ENCHANTMENT) - + if constants.CARD_TYPE_ARTIFACT in type_line: types.append(constants.CARD_TYPE_ARTIFACT) return types - -def DateCheck(date): + + +def check_date(date): + '''Checks a date string and returns false if the date is in the future''' result = True try: parts = date.split("-") @@ -158,66 +182,73 @@ def DateCheck(date): month = int(parts[1]) day = int(parts[2]) hour = 0 - - if datetime.datetime(year=year,month=month,day=day,hour=hour) > datetime.datetime.now(): + + if datetime.datetime(year=year, month=month, day=day, hour=hour) > datetime.datetime.now(): result = False - - except Exception as error: + + except Exception: result = False return result - -def DateShift(start_date, shifted_days, string_format): + + +def shift_date(start_date, shifted_days, string_format): + '''Shifts a date by a certain number of days''' shifted_date_string = "" shifted_date = datetime.date.min try: shifted_date = start_date + datetime.timedelta(days=shifted_days) - if string_format != None: + if string_format is not None: shifted_date_string = shifted_date.strftime(string_format) except Exception as error: - file_logger.info(f"DateShift Error: {error}") - + file_logger.info("shift_date Error: %s", error) + return shifted_date, shifted_date_string - -def ReleaseCheck(release_string, shifted_days): + + +def check_release_date(release_string, shifted_days): + '''Checks a shifted release data and returns false if the data is in the future''' result = True try: - release_date = datetime.datetime.strptime(release_string, "%Y-%m-%d").date() - shifted_release_date = DateShift(release_date, shifted_days, None)[0] + release_date = datetime.datetime.strptime( + release_string, "%Y-%m-%d").date() + shifted_release_date = shift_date(release_date, shifted_days, None)[0] today = datetime.date.today() - + if shifted_release_date > today: result = False - + except Exception as error: - file_logger.info(f"ReleaseCheck Error: {error}") - + file_logger.info("check_release_date Error: %s", error) + return result - -def FileIntegrityCheck(filename): + + +def check_file_integrity(filename): + '''Extracts data from a file to determine if it's formatted correctly''' result = Result.VALID json_data = {} - while(True): - #Check 1) File is present + while True: + # Check 1) File is present try: - with open(filename, 'r') as json_file: + with open(filename, 'r', encoding="utf-8", errors="replace") as json_file: json_data = json_file.read() - except Exception as error: + except Exception: result = Result.ERROR_MISSING_FILE break - - #Check 2) File contains required elements + + # Check 2) File contains required elements try: json_data = json.loads(json_data) - - #Check 2A) Meta data is present + + # Check 2A) Meta data is present version = json_data["meta"]["version"] if version == 1: json_data["meta"]["date_range"].split("->") else: - json_data["meta"]["start_date"] - json_data["meta"]["end_date"] - - #Check 2B) Card data is present + json_data["meta"]["start_date"] + json_data["meta"]["end_date"] + + # Check 2B) Card data is present cards = json_data["card_ratings"] for card in cards: cards[card][constants.DATA_FIELD_NAME] @@ -230,18 +261,21 @@ def FileIntegrityCheck(filename): cards[card][constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_ALSA] cards[card][constants.DATA_FIELD_DECK_COLORS][constants.FILTER_OPTION_ALL_DECKS][constants.DATA_FIELD_IWD] break - + if len(cards.keys()) < 100: result = Result.ERROR_UNREADABLE_FILE break - - except Exception as error: + + except Exception: result = Result.ERROR_UNREADABLE_FILE break break return result, json_data - + + class FileExtractor: + '''Class that handles the creation of set files and the retrieval of platform information''' + def __init__(self, directory): self.selected_sets = [] self.set_list = [] @@ -251,111 +285,130 @@ def __init__(self, directory): self.end_date = "" self.directory = directory self.context = ssl.SSLContext() - self.id = id self.card_ratings = {} - self.combined_data = {"meta" : {"collection_date" : str(datetime.datetime.now())}} + self.combined_data = { + "meta": {"collection_date": str(datetime.datetime.now())}} self.card_dict = {} self.deck_colors = constants.DECK_COLORS - def ClearData(self): - self.combined_data = {"meta" : {"collection_date" : str(datetime.datetime.now())}} + def clear_data(self): + '''Clear stored set information''' + self.combined_data = { + "meta": {"collection_date": str(datetime.datetime.now())}} self.card_dict = {} self.card_ratings = {} - def Sets(self, sets): + def select_sets(self, sets): + '''Public function that's used for setting class variables''' self.selected_sets = sets - - def SetList(self): + + def return_set_list(self): + '''Public function that's used for setting class variables''' if not self.set_list: - self.set_list = self.SessionSets() - + self.set_list = self.retrieve_set_list() + return self.set_list - def DraftType(self, draft_type): + + def set_draft_type(self, draft_type): + '''Public function that's used for setting class variables''' self.draft = draft_type - - def StartDate(self, start_date): + + def set_start_date(self, start_date): + '''Sets the start data in a set file''' result = False - if DateCheck(start_date): + if check_date(start_date): result = True self.start_date = start_date self.combined_data["meta"]["start_date"] = self.start_date return result - def EndDate(self, end_date): + + def set_end_date(self, end_date): + '''Sets the end date in a set file''' result = False - if DateCheck(end_date): + if check_date(end_date): result = True self.end_date = end_date self.combined_data["meta"]["end_date"] = self.end_date return result - - def Version(self, version): + + def set_version(self, version): + '''Sets the version in a set file''' self.combined_data["meta"]["version"] = version - - def DownloadCardData(self, ui_root, progress_bar, status, database_size): + + def download_card_data(self, ui_root, progress_bar, status, database_size): + '''Wrapper function for starting the set file download/creation process''' result = False result_string = "" temp_size = 0 try: - result, result_string, temp_size = self.DownloadExpansion(ui_root, progress_bar, status, database_size) - + result, result_string, temp_size = self._download_expansion( + ui_root, progress_bar, status, database_size) + except Exception as error: - file_logger.info(f"DownloadCardData Error: {error}") + file_logger.info("download_card_data Error: %s", error) result_string = error - + return result, result_string, temp_size - - def DownloadExpansion (self, ui_root, progress_bar, status, database_size): + + def _download_expansion(self, ui_root, progress_bar, status, database_size): + ''' Function that performs the following steps: + 1. Build a card data file from local Arena files (stored as temp_card_data.json in the Temp folder) + - The card sets contains the Arena IDs, card name, mana cost, colors, etc. + 1A. Collect the card data from Scryfall if it's unavailable locally (fallback) + 2. Collect the card_ratings data from scryfall + 3. Build a set file by combining the card data and the card ratings + ''' result = False result_string = "" temp_size = 0 try: - while(True): - progress_bar['value']=5 + while True: + progress_bar['value'] = 5 ui_root.update() - - result, result_string, temp_size = self.RetrieveLocalArenaData(ui_root, status, database_size) - if result == False: + result, result_string, temp_size = self._retrieve_local_arena_data( + ui_root, status, database_size) + + if not result: - result, result_string = self.SessionScryfallData(ui_root, status) - if result == False: + result, result_string = self.retrieve_scryfall_data( + ui_root, status) + if not result: break - - progress_bar['value']=10 + + progress_bar['value'] = 10 status.set("Collecting 17Lands Data") ui_root.update() - - if self.Session17Lands(self.selected_sets[constants.SET_LIST_17LANDS], - self.deck_colors, - ui_root, - progress_bar, - progress_bar['value'], - status) == False: + + if not self.retrieve_17lands_data(self.selected_sets[constants.SET_LIST_17LANDS], + self.deck_colors, + ui_root, + progress_bar, + progress_bar['value'], + status): result = False result_string = "Couldn't Collect 17Lands Data" break - matching_only = True if constants.SET_SELECTION_ALL in self.selected_sets[constants.SET_LIST_ARENA] else False - + matching_only = True if constants.SET_SELECTION_ALL in self.selected_sets[ + constants.SET_LIST_ARENA] else False + if not matching_only: - self.Initialize17LandsData() + self._initialize_17lands_data() status.set("Building Data Set File") ui_root.update() - if self.AssembleSetData(matching_only) == False: - result = False - result_string = "Couldn't Assemble Set Data" - break - + self._assemble_set(matching_only) break - + except Exception as error: - file_logger.info(f"DownloadExpansion Error: {error}") + file_logger.info("_download_expansion Error: %s", error) result_string = error - + return result, result_string, temp_size - - def RetrieveLocalArenaData(self, root, status, previous_database_size): + + def _retrieve_local_arena_data(self, root, status, previous_database_size): + '''Builds a card data file from raw Arena files''' result_string = "Couldn't Collect Local Card Data" result = False self.card_dict = {} @@ -363,275 +416,320 @@ def RetrieveLocalArenaData(self, root, status, previous_database_size): status.set("Searching Local Files") root.update() if sys.platform == constants.PLATFORM_ID_OSX: - directory = os.path.join(os.path.expanduser('~'), constants.LOCAL_DATA_FOLDER_PATH_OSX) if not self.directory else self.directory + directory = os.path.join(os.path.expanduser('~'), + constants.LOCAL_DATA_FOLDER_PATH_OSX) if not self.directory else self.directory paths = [os.path.join(directory, constants.LOCAL_DOWNLOADS_DATA)] else: if not self.directory: - path_list = [constants.WINDOWS_DRIVES, constants.WINDOWS_PROGRAM_FILES, [constants.LOCAL_DATA_FOLDER_PATH_WINDOWS]] - paths = [os.path.join(*x) for x in itertools.product(*path_list)] + path_list = [constants.WINDOWS_DRIVES, constants.WINDOWS_PROGRAM_FILES, [ + constants.LOCAL_DATA_FOLDER_PATH_WINDOWS]] + paths = [os.path.join(*x) + for x in itertools.product(*path_list)] else: - paths = [os.path.join(self.directory, constants.LOCAL_DOWNLOADS_DATA)] - - arena_cards_locations = SearchLocalFiles(paths, [constants.LOCAL_DATA_FILE_PREFIX_CARDS]) - arena_database_locations = SearchLocalFiles(paths, [constants.LOCAL_DATA_FILE_PREFIX_DATABASE]) - - if (not len(arena_cards_locations) or - not len(arena_database_locations)): - return result, result_string - - result = False - while(True): - try: - current_database_size = os.path.getsize(arena_cards_locations[0]) - + paths = [os.path.join( + self.directory, constants.LOCAL_DOWNLOADS_DATA)] + + arena_cards_locations = search_local_files( + paths, [constants.LOCAL_DATA_FILE_PREFIX_CARDS]) + arena_database_locations = search_local_files( + paths, [constants.LOCAL_DATA_FILE_PREFIX_DATABASE]) + + while True: + try: + if (not arena_cards_locations) or (not arena_database_locations): + break + + current_database_size = os.path.getsize( + arena_cards_locations[0]) + if current_database_size != previous_database_size: - file_logger.info(f"Local File Change Detected {current_database_size}, {previous_database_size}") - file_logger.info(f"Local Database Data: Searching file path {arena_database_locations[0]}") + file_logger.info( + "Local File Change Detected %d, %d", + current_database_size, previous_database_size) + file_logger.info( + "Local Database Data: Searching file path %s", + arena_database_locations[0]) status.set("Retrieving Localization Data") root.update() - result, card_text, card_enumerators = self.RetrieveLocalDatabase(arena_database_locations[0]) - + result, card_text, card_enumerators = self._retrieve_local_database( + arena_database_locations[0]) + if not result: break - - file_logger.info(f"Local Card Data: Searching file path {arena_cards_locations[0]}") + + file_logger.info( + "Local Card Data: Searching file path %s", + arena_cards_locations[0]) status.set("Retrieving Raw Card Data") root.update() - result, raw_card_data = self.RetrieveLocalCards(arena_cards_locations[0]) - + result, raw_card_data = self._retrieve_local_cards( + arena_cards_locations[0]) + if not result: break - status.set("Building Temporary Card Data File") + status.set("Building Temporary Card Data File") root.update() - result = self.AssembleStoredData(card_text, card_enumerators, raw_card_data) - + result = self._assemble_stored_data( + card_text, card_enumerators, raw_card_data) + if not result: break - - #Assemble information for local data set - status.set("Retrieving Temporary Card Data") - root.update() - result = self.RetrieveStoredData(self.selected_sets[constants.SET_LIST_ARENA]) + + # Assemble information for local data set + status.set("Retrieving Temporary Card Data") + root.update() + result = self._retrieve_stored_data( + self.selected_sets[constants.SET_LIST_ARENA]) database_size = current_database_size - + except Exception as error: - file_logger.info(f"RetrieveLocalArenaData Error: {error}") + file_logger.info("_retrieve_local_arena_data Error: %s", error) break - + if not result: file_logger.info(result_string) - + return result, result_string, database_size - def RetrieveLocalCards(self, file_location): + def _retrieve_local_cards(self, file_location): + '''Function that retrieves pertinent card data from raw Arena files''' result = False card_data = {} try: - with open(file_location, 'r', encoding="utf8") as json_file: + with open(file_location, 'r', encoding="utf-8", errors="replace") as json_file: json_data = json.loads(json_file.read()) - + for card in json_data: - card = {k.lower(): v for k, v in card.items()} #Making all of the keys lowercase - if(True): - #if ((card_set == "All") or - #(card_set in card[constants.LOCAL_CARDS_KEY_SET]) or - #((constants.LOCAL_CARDS_KEY_DIGITAL_RELEASE_SET in card) and (card_set in card[constants.LOCAL_CARDS_KEY_DIGITAL_RELEASE_SET]))): - try: - set = card[constants.LOCAL_CARDS_KEY_SET] - - if set not in card_data: - card_data[set] = {} - - if constants.LOCAL_CARDS_KEY_TOKEN in card: + # Making all of the keys lowercase + card = {k.lower(): v for k, v in card.items()} + try: + card_set = card[constants.LOCAL_CARDS_KEY_SET] + + if ((constants.LOCAL_CARDS_KEY_DIGITAL_RELEASE_SET in card) and + (re.findall("^[yY]\d{2}$", card_set, re.DOTALL))): + card_set = card[constants.LOCAL_CARDS_KEY_DIGITAL_RELEASE_SET] + + if card_set not in card_data: + card_data[card_set] = {} + + if constants.LOCAL_CARDS_KEY_TOKEN in card: + continue + + group_id = card[constants.LOCAL_CARDS_KEY_GROUP_ID] + + if constants.LOCAL_CARDS_KEY_LINKED_FACES in card: + linked_id = int( + card[constants.LOCAL_CARDS_KEY_LINKED_FACES].split(',')[0]) + if linked_id < group_id: + # The application will no longer list the names of all the card faces. This will address an issue with excessively long tooltips for specialize cards + # self.card_dict[card["linkedFaces"][0]][constants.DATA_FIELD_NAME].append(card["titleId"]) + types = [int(x) for x in card[constants.LOCAL_CARDS_KEY_TYPES].split( + ',')] if constants.LOCAL_CARDS_KEY_TYPES in card else [] + card_data[card_set][linked_id][constants.LOCAL_CARDS_KEY_TYPES].extend( + types) continue - - group_id = card[constants.LOCAL_CARDS_KEY_GROUP_ID] - - if constants.LOCAL_CARDS_KEY_LINKED_FACES in card: - linked_id = int(card[constants.LOCAL_CARDS_KEY_LINKED_FACES].split(',')[0]) - if linked_id < group_id: - #The application will no longer list the names of all the card faces. This will address an issue with excessively long tooltips for specialize cards - #self.card_dict[card["linkedFaces"][0]][constants.DATA_FIELD_NAME].append(card["titleId"]) - types = [int(x) for x in card[constants.LOCAL_CARDS_KEY_TYPES].split(',')] if constants.LOCAL_CARDS_KEY_TYPES in card else [] - card_data[set][linked_id][constants.LOCAL_CARDS_KEY_TYPES].extend(types) - continue - - card_data[set][group_id] = {constants.DATA_FIELD_NAME : [card[constants.LOCAL_CARDS_KEY_TITLE_ID]], constants.DATA_SECTION_IMAGES : []} - card_data[set][group_id][constants.DATA_FIELD_TYPES] = [int(x) for x in card[constants.LOCAL_CARDS_KEY_TYPES].split(',')] if constants.LOCAL_CARDS_KEY_TYPES in card else [] - card_data[set][group_id][constants.DATA_FIELD_COLORS] = [int(x) for x in card[constants.LOCAL_CARDS_KEY_COLOR_ID].split(',')] if constants.LOCAL_CARDS_KEY_COLOR_ID in card else [] - mana_cost, cmc = DecodeManaCost(card[constants.LOCAL_CARDS_KEY_CASTING_COST]) if constants.LOCAL_CARDS_KEY_CASTING_COST in card else ("",0) - card_data[set][group_id][constants.DATA_FIELD_CMC] = cmc - card_data[set][group_id]["mana_cost"] = mana_cost - - result = True - except Exception as error: - file_logger.info(f"Card Read Error: {card}") - break - #pass + + card_data[card_set][group_id] = {constants.DATA_FIELD_NAME: [ + card[constants.LOCAL_CARDS_KEY_TITLE_ID]], constants.DATA_SECTION_IMAGES: []} + card_data[card_set][group_id][constants.DATA_FIELD_TYPES] = [int( + x) for x in card[constants.LOCAL_CARDS_KEY_TYPES].split(',')] if constants.LOCAL_CARDS_KEY_TYPES in card else [] + card_data[card_set][group_id][constants.DATA_FIELD_COLORS] = [int( + x) for x in card[constants.LOCAL_CARDS_KEY_COLOR_ID].split(',')] if constants.LOCAL_CARDS_KEY_COLOR_ID in card else [] + mana_cost, cmc = decode_mana_cost( + card[constants.LOCAL_CARDS_KEY_CASTING_COST]) if constants.LOCAL_CARDS_KEY_CASTING_COST in card else ("", 0) + card_data[card_set][group_id][constants.DATA_FIELD_CMC] = cmc + card_data[card_set][group_id]["mana_cost"] = mana_cost + card_data[card_set][group_id][constants.DATA_FIELD_RARITY] = constants.CARD_RARITY_DICT[card[constants.LOCAL_CARDS_KEY_RARITY] + ] if constants.LOCAL_CARDS_KEY_RARITY in card else constants.CARD_RARITY_COMMON + + result = True + except Exception as error: + file_logger.info( + "Card Read Error: %s, %s", error, card) + break + # pass except Exception as error: - file_logger.info(f"RetrieveLocalCards Error: {error}") - + file_logger.info("_retrieve_local_cards Error: %s", error) + return result, card_data - - def RetrieveLocalDatabase(self, file_location): + + def _retrieve_local_database(self, file_location): + '''Retrieves localization and enumeration data from an Arena database''' result = False card_text = {} card_enumerators = {} try: - #Open Sqlite3 database - while(True): + # Open Sqlite3 database + while True: connection = sqlite3.connect(file_location) connection.row_factory = sqlite3.Row cursor = connection.cursor() - - rows = [dict(row) for row in cursor.execute(constants.LOCAL_DATABASE_LOCALIZATION_QUERY)] - + + rows = [dict(row) for row in cursor.execute( + constants.LOCAL_DATABASE_LOCALIZATION_QUERY)] + if not rows: break - - result, card_text = self.RetrieveLocalCardText(rows) - + + result, card_text = self._retrieve_local_card_text(rows) + if not result: break - - - rows = [dict(row) for row in cursor.execute(constants.LOCAL_DATABASE_ENUMERATOR_QUERY)] - + + rows = [dict(row) for row in cursor.execute( + constants.LOCAL_DATABASE_ENUMERATOR_QUERY)] + if not rows: break - - result, card_enumerators = self.RetrieveLocalCardEnumerators(rows) - + + result, card_enumerators = self._retrieve_local_card_enumerators( + rows) + if not result: break - - #store the localization data in a temporary file - #with open(constants.TEMP_LOCALIZATION_FILE, 'w', encoding='utf-8') as json_file: - # json.dump(card_data, json_file) - # - #result = True + break - + except Exception as error: result = False - file_logger.info(f"RetrieveLocalDatabase Error: {error}") - + file_logger.info("_retrieve_local_database Error: %s", error) + return result, card_text, card_enumerators - - def RetrieveLocalCardText(self, data): + + def _retrieve_local_card_text(self, data): + '''Returns a dict containing localization data''' result = True card_text = {} try: - #Retrieve the title (card name) for each of the collected arena IDs - card_text = {x[constants.LOCAL_DATABASE_LOCALIZATION_COLUMN_ID] : x[constants.LOCAL_DATABASE_LOCALIZATION_COLUMN_TEXT] for x in data} - + # Retrieve the title (card name) for each of the collected arena IDs + card_text = {x[constants.LOCAL_DATABASE_LOCALIZATION_COLUMN_ID]: x[constants.LOCAL_DATABASE_LOCALIZATION_COLUMN_TEXT] for x in data} + except Exception as error: result = False - file_logger.info(f"RetrieveLocalCardText Error: {error}") - + file_logger.info("_retrieve_local_card_text Error: %s", error) + return result, card_text - - def RetrieveLocalCardEnumerators(self, data): + + def _retrieve_local_card_enumerators(self, data): + '''Returns a dict containing card enumeration data''' result = True - card_enumerators = {constants.DATA_FIELD_COLORS : {}, constants.DATA_FIELD_TYPES : {}} + card_enumerators = {constants.DATA_FIELD_COLORS: {}, + constants.DATA_FIELD_TYPES: {}} try: for row in data: - if row[constants.LOCAL_DATABASE_ENUMERATOR_COLUMN_TYPE] == constants.LOCAL_DATABASE_ENUMERATOR_TYPE_CARD_TYPES: - card_enumerators[constants.DATA_FIELD_TYPES][row[constants.LOCAL_DATABASE_ENUMERATOR_COLUMN_VALUE]] = row[constants.LOCAL_DATABASE_ENUMERATOR_COLUMN_ID] - elif row[constants.LOCAL_DATABASE_ENUMERATOR_COLUMN_TYPE] == constants.LOCAL_DATABASE_ENUMERATOR_TYPE_COLOR: - card_enumerators[constants.DATA_FIELD_COLORS][row[constants.LOCAL_DATABASE_ENUMERATOR_COLUMN_VALUE]] = row[constants.LOCAL_DATABASE_ENUMERATOR_COLUMN_ID] + if row[constants.LOCAL_DATABASE_ENUMERATOR_COLUMN_TYPE] == constants.LOCAL_DATABASE_ENUMERATOR_TYPE_CARD_TYPES: + card_enumerators[constants.DATA_FIELD_TYPES][row[constants.LOCAL_DATABASE_ENUMERATOR_COLUMN_VALUE] + ] = row[constants.LOCAL_DATABASE_ENUMERATOR_COLUMN_ID] + elif row[constants.LOCAL_DATABASE_ENUMERATOR_COLUMN_TYPE] == constants.LOCAL_DATABASE_ENUMERATOR_TYPE_COLOR: + card_enumerators[constants.DATA_FIELD_COLORS][row[constants.LOCAL_DATABASE_ENUMERATOR_COLUMN_VALUE] + ] = row[constants.LOCAL_DATABASE_ENUMERATOR_COLUMN_ID] except Exception as error: result = False - file_logger.info(f"RetrieveLocalCardEnumerators Error: {error}") - + file_logger.info( + "_retrieve_local_card_enumerators Error: %s", error) + return result, card_enumerators - - def AssembleStoredData(self, card_text, card_enumerators, card_data): + + def _assemble_stored_data(self, card_text, card_enumerators, card_data): + '''Creates a temporary card data file from data collected from local Arena files''' result = False try: for card_set in card_data: for card in card_data[card_set]: try: - card_data[card_set][card][constants.DATA_FIELD_NAME] = " // ".join(card_text[x] for x in card_data[card_set][card][constants.DATA_FIELD_NAME]) - card_data[card_set][card][constants.DATA_FIELD_TYPES] = list(set([card_text[card_enumerators[constants.DATA_FIELD_TYPES][x]] for x in card_data[card_set][card][constants.DATA_FIELD_TYPES]])) - card_data[card_set][card][constants.DATA_FIELD_COLORS] = [constants.CARD_COLORS_DICT[card_text[card_enumerators[constants.DATA_FIELD_COLORS][x]]] for x in card_data[card_set][card][constants.DATA_FIELD_COLORS]] + card_data[card_set][card][constants.DATA_FIELD_NAME] = " // ".join( + card_text[x] for x in card_data[card_set][card][constants.DATA_FIELD_NAME]) + card_data[card_set][card][constants.DATA_FIELD_TYPES] = list(set( + [card_text[card_enumerators[constants.DATA_FIELD_TYPES][x]] for x in card_data[card_set][card][constants.DATA_FIELD_TYPES]])) + card_data[card_set][card][constants.DATA_FIELD_COLORS] = [ + constants.CARD_COLORS_DICT[card_text[card_enumerators[constants.DATA_FIELD_COLORS][x]]] for x in card_data[card_set][card][constants.DATA_FIELD_COLORS]] if constants.CARD_TYPE_CREATURE in card_data[card_set][card][constants.DATA_FIELD_TYPES]: - index = card_data[card_set][card][constants.DATA_FIELD_TYPES].index(constants.CARD_TYPE_CREATURE) - card_data[card_set][card][constants.DATA_FIELD_TYPES].insert(0, card_data[card_set][card][constants.DATA_FIELD_TYPES].pop(index)) + index = card_data[card_set][card][constants.DATA_FIELD_TYPES].index( + constants.CARD_TYPE_CREATURE) + card_data[card_set][card][constants.DATA_FIELD_TYPES].insert( + 0, card_data[card_set][card][constants.DATA_FIELD_TYPES].pop(index)) result = True - except Exception as error: + except Exception: pass - + if result: - #Store all of the processed card data - with open(constants.TEMP_CARD_DATA_FILE, 'w', encoding='utf-8') as json_file: + # Store all of the processed card data + with open(constants.TEMP_CARD_DATA_FILE, 'w', encoding="utf-8", errors="replace") as json_file: json.dump(card_data, json_file) - + except Exception as error: result = False - file_logger.info(f"AssembleStoredData Error: {error}") - + file_logger.info("_assemble_stored_data Error: %s", error) + return result - - def RetrieveStoredData(self, set_list): + + def _retrieve_stored_data(self, set_list): + '''Retrieves card data from the temp_card_data.json file stored in the Temp folder''' result = False self.card_dict = {} try: - with open(constants.TEMP_CARD_DATA_FILE, 'r', encoding='utf-8') as data: + with open(constants.TEMP_CARD_DATA_FILE, 'r', encoding="utf-8", errors="replace") as data: json_file = data.read() json_data = json.loads(json_file) - + if constants.SET_SELECTION_ALL in set_list: - for set in json_data: - self.card_dict.update(json_data[set].copy()) + for card_data in json_data.values(): + self.card_dict.update(card_data.copy()) else: - for set in set_list: - if set in json_data: - self.card_dict.update(json_data[set].copy()) - + for search_set in set_list: + matching_sets = list( + filter(lambda x, ss=search_set: ss in x, json_data)) + for match in matching_sets: + self.card_dict.update(json_data[match].copy()) + if len(self.card_dict): result = True - + except Exception as error: result = False - file_logger.info(f"AssembleStoredData Error: {error}") - + file_logger.info("_retrieve_stored_data Error: %s", error) + return result - - def SessionRepositoryVersion(self): + + def retrieve_repository_version(self): + '''Read the version.txt file in Github to determine the latest application version''' version = "" try: url = "https://raw.github.com/bstaple1/MTGA_Draft_17Lands/master/version.txt" url_data = urllib.request.urlopen(url, context=self.context).read() - - version = self.ProcessRepositoryVersionData(url_data) - + + version = self._process_repository_version(url_data) + except Exception as error: - file_logger.info(f"SessionRepositoryVersion Error: {error}") + file_logger.info("retrieve_repository_version Error: %s", error) return version - def SessionRepositoryDownload(self, filename): - version = "" + def retrieve_repository_file(self, filename): + '''Download a file from Github''' + result = True try: - url = "https://raw.github.com/bstaple1/MTGA_Draft_17Lands/master/%s" % filename + url = f"https://raw.github.com/bstaple1/MTGA_Draft_17Lands/master/{filename}" url_data = urllib.request.urlopen(url, context=self.context).read() - - with open(filename,'wb') as file: - file.write(url_data) - except Exception as error: - file_logger.info(f"SessionRepositoryDownload Error: {error}") - return version + with open(filename, 'wb', encoding="utf-8", errors="replace") as file: + file.write(url_data) + except Exception as error: + file_logger.info("retrieve_repository_file Error: %s", error) + result = False + return result - def SessionScryfallData(self, root, status): + def retrieve_scryfall_data(self, root, status): + '''Use the Scryfall API to retrieve the set data needed for building a card set file* + - This is a fallback feature feature that's used in case there's an issue with the local Arena files + ''' result = False self.card_dict = {} result_string = "Couldn't Retrieve Card Data" url = "" - for set in self.selected_sets[constants.SET_LIST_SCRYFALL]: + for card_set in self.selected_sets[constants.SET_LIST_SCRYFALL]: if set == "dbl": continue retry = constants.SCRYFALL_REQUEST_ATTEMPT_MAX @@ -639,85 +737,97 @@ def SessionScryfallData(self, root, status): try: status.set("Collecting Scryfall Data") root.update() - #https://api.scryfall.com/cards/search?order=set&unique=prints&q=e%3AMID - url = "https://api.scryfall.com/cards/search?order=set&unique=prints&q=e" + urlencode(':', safe='') + "%s" % (set) - url_data = urllib.request.urlopen(url, context=self.context).read() - + # https://api.scryfall.com/cards/search?order=set&unique=prints&q=e%3AMID + url = "https://api.scryfall.com/cards/search?order=set&unique=prints&q=e" + \ + urlencode(':', safe='') + f"{card_set}" + url_data = urllib.request.urlopen( + url, context=self.context).read() + set_json_data = json.loads(url_data) - - result, result_string = self.ProcessScryfallData(set_json_data["data"]) - - while (set_json_data["has_more"] == True) and (result == True): + + result, result_string = self._process_scryfall_data( + set_json_data["data"]) + + while set_json_data["has_more"] and result: url = set_json_data["next_page"] - url_data = urllib.request.urlopen(url, context=self.context).read() + url_data = urllib.request.urlopen( + url, context=self.context).read() set_json_data = json.loads(url_data) - result, result_string = self.ProcessScryfallData(set_json_data["data"]) - - - if result == True: + result, result_string = self._process_scryfall_data( + set_json_data["data"]) + + if result: break - + except Exception as error: - file_logger.info(url) - file_logger.info(f"SessionScryfallData Error: {error}") - - if result == False: + file_logger.info(url) + file_logger.info("retrieve_scryfall_data Error: %s", error) + + if not result: retry -= 1 - + if retry: attempt_count = constants.CARD_RATINGS_ATTEMPT_MAX - retry - status.set(f"""Collecting Scryfall Data - Request Failed ({attempt_count}/{constants.SCRYFALL_REQUEST_ATTEMPT_MAX}) - Retry in {constants.SCRYFALL_REQUEST_BACKOFF_DELAY_SECONDS} seconds""") + status.set( + f"""Collecting Scryfall Data - Request Failed ({attempt_count}/{constants.SCRYFALL_REQUEST_ATTEMPT_MAX}) - Retry in {constants.SCRYFALL_REQUEST_BACKOFF_DELAY_SECONDS} seconds""") root.update() - time.sleep(constants.SCRYFALL_REQUEST_BACKOFF_DELAY_SECONDS) + time.sleep( + constants.SCRYFALL_REQUEST_BACKOFF_DELAY_SECONDS) return result, result_string - def Initialize17LandsData(self): - for card in self.card_dict: - self.card_dict[card][constants.DATA_FIELD_DECK_COLORS] = {} + def _initialize_17lands_data(self): + '''Initialize the 17Lands data by setting the fields to 0 in case there are gaps in the downloaded card data''' + for data in self.card_dict.values(): + data[constants.DATA_FIELD_DECK_COLORS] = {} for color in self.deck_colors: - self.card_dict[card][constants.DATA_FIELD_DECK_COLORS][color] = {x : 0.0 for x in constants.DATA_FIELD_17LANDS_DICT.keys() if x != constants.DATA_SECTION_IMAGES} - + data[constants.DATA_FIELD_DECK_COLORS][color] = { + x: 0.0 for x in constants.DATA_FIELD_17LANDS_DICT if x != constants.DATA_SECTION_IMAGES} - def Session17Lands(self, sets, deck_colors, root, progress, initial_progress, status): + def retrieve_17lands_data(self, sets, deck_colors, root, progress, initial_progress, status): + '''Use the 17Lands endpoint to download the card ratings data for all of the deck filter options''' self.card_ratings = {} current_progress = 0 result = False url = "" - for set in sets: - if set == "dbl": + for set_code in sets: + if set_code == "dbl": continue for color in deck_colors: retry = constants.CARD_RATINGS_ATTEMPT_MAX result = False while retry: - + try: status.set(f"Collecting {color} 17Lands Data") root.update() - url = f"https://www.17lands.com/card_ratings/data?expansion={set}&format={self.draft}&start_date={self.start_date}&end_date={self.end_date}" - + url = f"https://www.17lands.com/card_ratings/data?expansion={set_code}&format={self.draft}&start_date={self.start_date}&end_date={self.end_date}" + if color != constants.FILTER_OPTION_ALL_DECKS: url += "&colors=" + color - url_data = urllib.request.urlopen(url, context=self.context).read() - + url_data = urllib.request.urlopen( + url, context=self.context).read() + set_json_data = json.loads(url_data) - self.Retrieve17Lands(color, set_json_data) + self._process_17lands_data(color, set_json_data) result = True break except Exception as error: - file_logger.info(url) - file_logger.info(f"Session17Lands Error: {error}") + file_logger.info(url) + file_logger.info( + "retrieve_17lands_data Error: %s", error) retry -= 1 - + if retry: attempt_count = constants.CARD_RATINGS_ATTEMPT_MAX - retry - status.set(f"""Collecting {color} 17Lands Data - Request Failed ({attempt_count}/{constants.CARD_RATINGS_ATTEMPT_MAX}) - Retry in {constants.CARD_RATINGS_BACKOFF_DELAY_SECONDS} seconds""") + status.set( + f"""Collecting {color} 17Lands Data - Request Failed ({attempt_count}/{constants.CARD_RATINGS_ATTEMPT_MAX}) - Retry in {constants.CARD_RATINGS_BACKOFF_DELAY_SECONDS} seconds""") root.update() - time.sleep(constants.CARD_RATINGS_BACKOFF_DELAY_SECONDS) + time.sleep( + constants.CARD_RATINGS_BACKOFF_DELAY_SECONDS) - if result: - current_progress += 3 / len(self.selected_sets[constants.SET_LIST_17LANDS]) + current_progress += (3 / + len(self.selected_sets[constants.SET_LIST_17LANDS])) progress['value'] = current_progress + initial_progress root.update() else: @@ -726,182 +836,205 @@ def Session17Lands(self, sets, deck_colors, root, progress, initial_progress, st if set == "stx": break - return result - - def AssembleSetData(self, matching_only): + return result + + def _assemble_set(self, matching_only): + '''Combine the 17Lands ratings and the card data to form the complete set data''' self.combined_data["card_ratings"] = {} - for card in self.card_dict: - if self.ProcessCardRatings(self.card_dict[card]): - self.combined_data["card_ratings"][card] = self.card_dict[card] + for card, card_data in self.card_dict.items(): + if self._process_card_data(card_data): + self.combined_data["card_ratings"][card] = card_data elif not matching_only: - self.combined_data["card_ratings"][card] = self.card_dict[card] - - def SessionSets(self): + self.combined_data["card_ratings"][card] = card_data + + def retrieve_set_list(self): + '''Use the Scryfall set API to collect a list of Magic sets + - The application only recognizes sets that it can collect using this API + - The application will automatically support any new set that can be retrieved using this API + ''' sets = {} try: url = "https://api.scryfall.com/sets" url_data = urllib.request.urlopen(url, context=self.context).read() - + set_json_data = json.loads(url_data) - sets = self.ProcessSetData(sets, set_json_data["data"]) - while set_json_data["has_more"] == True: + sets = self._process_set_data(sets, set_json_data["data"]) + while set_json_data["has_more"]: url = set_json_data["next_page"] - url_data = urllib.request.urlopen(url, context=self.context).read() + url_data = urllib.request.urlopen( + url, context=self.context).read() set_json_data = json.loads(url_data) - sets = self.ProcessSetData(sets, set_json_data["data"]) - - + sets = self._process_set_data(sets, set_json_data["data"]) + except Exception as error: - file_logger.info(f"SessionSets Error: {error}") - return sets - def SessionColorRatings(self): + file_logger.info("retrieve_set_list Error: %s", error) + return sets + + def retrieve_17lands_color_ratings(self): + '''Use 17Lands endpoint to collect the data from the color_ratings page''' try: - #https://www.17lands.com/color_ratings/data?expansion=VOW&event_type=QuickDraft&start_date=2019-1-1&end_date=2022-01-13&combine_splash=true + # https://www.17lands.com/color_ratings/data?expansion=VOW&event_type=QuickDraft&start_date=2019-1-1&end_date=2022-01-13&combine_splash=true url = f"https://www.17lands.com/color_ratings/data?expansion={self.selected_sets[constants.SET_LIST_17LANDS][0]}&event_type={self.draft}&start_date={self.start_date}&end_date={self.end_date}&combine_splash=true" url_data = urllib.request.urlopen(url, context=self.context).read() - + color_json_data = json.loads(url_data) - self.RetrieveColorRatings(color_json_data) - + self._process_17lands_color_ratings(color_json_data) + except Exception as error: - file_logger.info(url) - file_logger.info(f"SessionColorRatings Error: {error}") - - def Retrieve17Lands(self, colors, cards): + file_logger.info(url) + file_logger.info("retrieve_17lands_color_ratings Error: %s", error) + + def _process_17lands_data(self, colors, cards): + '''Parse the 17Lands json data to extract the card ratings''' result = True for card in cards: try: - card_data = {constants.DATA_SECTION_RATINGS : [], constants.DATA_SECTION_IMAGES : []} - color_data = {colors : {}} - for k, v in constants.DATA_FIELD_17LANDS_DICT.items(): - if k == constants.DATA_SECTION_IMAGES: - for field in v: + card_data = {constants.DATA_SECTION_RATINGS: [], + constants.DATA_SECTION_IMAGES: []} + color_data = {colors: {}} + for key, value in constants.DATA_FIELD_17LANDS_DICT.items(): + if key == constants.DATA_SECTION_IMAGES: + for field in value: if field in card and len(card[field]): - image_url = f"{constants.URL_17LANDS}{card[field]}" if card[field].startswith(constants.IMAGE_17LANDS_SITE_PREFIX) else card[field] - card_data[constants.DATA_SECTION_IMAGES].append(image_url) - elif v in card: - if (k in constants.WIN_RATE_OPTIONS) or (k == constants.DATA_FIELD_IWD): - color_data[colors][k] = round(float(card[v]) * 100.0, 2) if card[v] != None else 0.0 - elif ((k == constants.DATA_FIELD_ATA) or - (k == constants.DATA_FIELD_ALSA)): - color_data[colors][k] = round(float(card[v]), 2) + image_url = f"{constants.URL_17LANDS}{card[field]}" if card[field].startswith( + constants.IMAGE_17LANDS_SITE_PREFIX) else card[field] + card_data[constants.DATA_SECTION_IMAGES].append( + image_url) + elif value in card: + if (key in constants.WIN_RATE_OPTIONS) or (key == constants.DATA_FIELD_IWD): + color_data[colors][key] = round( + float(card[value]) * 100.0, 2) if card[value] is not None else 0.0 + elif ((key == constants.DATA_FIELD_ATA) or + (key == constants.DATA_FIELD_ALSA)): + color_data[colors][key] = round( + float(card[value]), 2) else: - color_data[colors][k] = int(card[v]) + color_data[colors][key] = int(card[value]) card_name = card[constants.DATA_FIELD_NAME] - + if card_name not in self.card_ratings: self.card_ratings[card_name] = card_data - - self.card_ratings[card_name][constants.DATA_SECTION_RATINGS].append(color_data) - + + self.card_ratings[card_name][constants.DATA_SECTION_RATINGS].append( + color_data) + except Exception as error: result = False - file_logger.info(f"Retrieve17Lands Error: {error}") - - return result - - def RetrieveColorRatings(self, colors): + file_logger.info("_process_17lands_data Error: %s", error) + + return result + + def _process_17lands_color_ratings(self, colors): + '''Parse the 17Lands json data to collect the color ratings''' color_ratings_dict = { - "Mono-White" : constants.CARD_COLOR_SYMBOL_WHITE, - "Mono-Blue" : constants.CARD_COLOR_SYMBOL_BLUE, - "Mono-Black" : constants.CARD_COLOR_SYMBOL_BLACK, - "Mono-Red" : constants.CARD_COLOR_SYMBOL_RED, - "Mono-Green" : constants.CARD_COLOR_SYMBOL_GREEN, - "(WU)" : "WU", - "(UB)" : "UB", - "(BR)" : "BR", - "(RG)" : "RG", - "(GW)" : "GW", - "(WB)" : "WB", - "(BG)" : "BG", - "(GU)" : "GU", - "(UR)" : "UR", - "(RW)" : "RW", - "(WUR)" : "WUR", - "(UBG)" : "UBG", - "(BRW)" : "BRW", - "(RGU)" : "RGU", - "(GWB)" : "GWB", - "(WUB)" : "WUB", - "(UBR)" : "UBR", - "(BRG)" : "BRG", - "(RGW)" : "RGW", - "(GWU)" : "GWU", + "Mono-White": constants.CARD_COLOR_SYMBOL_WHITE, + "Mono-Blue": constants.CARD_COLOR_SYMBOL_BLUE, + "Mono-Black": constants.CARD_COLOR_SYMBOL_BLACK, + "Mono-Red": constants.CARD_COLOR_SYMBOL_RED, + "Mono-Green": constants.CARD_COLOR_SYMBOL_GREEN, + "(WU)": "WU", + "(UB)": "UB", + "(BR)": "BR", + "(RG)": "RG", + "(GW)": "GW", + "(WB)": "WB", + "(BG)": "BG", + "(GU)": "GU", + "(UR)": "UR", + "(RW)": "RW", + "(WUR)": "WUR", + "(UBG)": "UBG", + "(BRW)": "BRW", + "(RGU)": "RGU", + "(GWB)": "GWB", + "(WUB)": "WUB", + "(UBR)": "UBR", + "(BRG)": "BRG", + "(RGW)": "RGW", + "(GWU)": "GWU", } - + try: self.combined_data["color_ratings"] = {} for color in colors: games = color["games"] - if (color["is_summary"] == False) and (games > 5000): + if not color["is_summary"] and (games > 5000): color_name = color["color_name"] - winrate = round((float(color["wins"])/color["games"]) * 100, 1) - - color_label = [x for x in color_ratings_dict.keys() if x in color_name] + winrate = round( + (float(color["wins"])/color["games"]) * 100, 1) + + color_label = [ + x for x in color_ratings_dict if x in color_name] + + if color_label: - if len(color_label): - processed_colors = color_ratings_dict[color_label[0]] - - if processed_colors not in self.combined_data["color_ratings"].keys(): + + if processed_colors not in self.combined_data["color_ratings"]: self.combined_data["color_ratings"][processed_colors] = winrate - + except Exception as error: - file_logger.info(f"RetrieveColorRatings Error: {error}") + file_logger.info("_process_17lands_color_ratings Error: %s", error) - - def ProcessScryfallData (self, data): + def _process_scryfall_data(self, data): + '''Parse json data from the Scryfall API to extract pertinent card data''' result = False result_string = "Scryfall Data Unavailable" for card_data in data: try: if "arena_id" not in card_data: continue - + arena_id = card_data["arena_id"] self.card_dict[arena_id] = { - constants.DATA_FIELD_NAME : card_data[constants.DATA_FIELD_NAME], - constants.DATA_FIELD_CMC : card_data[constants.DATA_FIELD_CMC], - constants.DATA_FIELD_COLORS : card_data["color_identity"], - constants.DATA_FIELD_TYPES : ExtractTypes(card_data["type_line"]), - "mana_cost" : 0, - constants.DATA_SECTION_IMAGES : [], + constants.DATA_FIELD_NAME: card_data[constants.DATA_FIELD_NAME], + constants.DATA_FIELD_CMC: card_data[constants.DATA_FIELD_CMC], + constants.DATA_FIELD_COLORS: card_data["color_identity"], + constants.DATA_FIELD_TYPES: extract_types(card_data["type_line"]), + "mana_cost": 0, + constants.DATA_SECTION_IMAGES: [], } if "card_faces" in card_data: self.card_dict[arena_id]["mana_cost"] = card_data["card_faces"][0]["mana_cost"] - self.card_dict[arena_id][constants.DATA_SECTION_IMAGES].append(card_data["card_faces"][0]["image_uris"]["normal"]) - self.card_dict[arena_id][constants.DATA_SECTION_IMAGES].append(card_data["card_faces"][1]["image_uris"]["normal"]) - + self.card_dict[arena_id][constants.DATA_SECTION_IMAGES].append( + card_data["card_faces"][0]["image_uris"]["normal"]) + self.card_dict[arena_id][constants.DATA_SECTION_IMAGES].append( + card_data["card_faces"][1]["image_uris"]["normal"]) + else: self.card_dict[arena_id]["mana_cost"] = card_data["mana_cost"] - self.card_dict[arena_id][constants.DATA_SECTION_IMAGES] = [card_data["image_uris"]["normal"]] + self.card_dict[arena_id][constants.DATA_SECTION_IMAGES] = [ + card_data["image_uris"]["normal"]] result = True except Exception as error: - file_logger.info(f"ProcessScryfallData Error: {error}") + file_logger.info("_process_scryfall_data Error: %s", error) result_string = error return result, result_string - - def ProcessCardRatings (self, card): + + def _process_card_data(self, card): + '''Link the 17Lands card ratings with the card data''' result = False try: - card_sides = card[constants.DATA_FIELD_NAME].split(" // ") + card_sides = card[constants.DATA_FIELD_NAME].split(" // ") card_sides = [x.replace("///", "//") for x in card_sides] - matching_cards = [x for x in self.card_ratings.keys() if x in card_sides] - if(matching_cards): + matching_cards = [ + x for x in self.card_ratings if x in card_sides] + if matching_cards: ratings_card_name = matching_cards[0] deck_colors = self.card_ratings[ratings_card_name][constants.DATA_SECTION_RATINGS] - + card[constants.DATA_SECTION_IMAGES] = self.card_ratings[ratings_card_name][constants.DATA_SECTION_IMAGES] card[constants.DATA_FIELD_DECK_COLORS] = {} for color in self.deck_colors: - card[constants.DATA_FIELD_DECK_COLORS][color] = {x : 0.0 for x in constants.DATA_FIELD_17LANDS_DICT.keys() if x != constants.DATA_SECTION_IMAGES} + card[constants.DATA_FIELD_DECK_COLORS][color] = { + x: 0.0 for x in constants.DATA_FIELD_17LANDS_DICT if x != constants.DATA_SECTION_IMAGES} result = True for deck_color in deck_colors: for key, value in deck_color.items(): @@ -909,80 +1042,101 @@ def ProcessCardRatings (self, card): card[constants.DATA_FIELD_DECK_COLORS][key][field] = value[field] except Exception as error: - file_logger.info(f"ProcessCardRatings Error: {error}") + file_logger.info("_process_card_data Error: %s", error) return result - - def ProcessSetData (self, sets, data): + + def _process_set_data(self, sets, data): + '''Parse the Scryfall set list data to extract information for sets that are supported by Arena + - Scryfall lists all Magic sets (paper and digital), so the application needs to filter out non-Arena sets + - The application will only display sets that have been released + ''' counter = 0 - for set in data: + for card_set in data: try: - #Check if the set has been released - if "released_at" in set and not ReleaseCheck(set["released_at"], constants.SET_RELEASE_OFFSET_DAYS): - continue - - set_name = set[constants.DATA_FIELD_NAME] - set_code = set["code"] - + # Check if the set has been released + if "released_at" in card_set: + # Sets that are not digitial only are release in Arena 1 week before paper + start_offset = constants.SET_RELEASE_OFFSET_DAYS if not card_set[ + "digital"] else 0 + if not check_release_date(card_set["released_at"], start_offset): + continue + + set_name = card_set[constants.DATA_FIELD_NAME] + set_code = card_set["code"] + if set_code == "dbl": sets[set_name] = {} - sets[set_name][constants.SET_LIST_ARENA] = ["VOW","MID"] - sets[set_name][constants.SET_LIST_17LANDS] = [set_code.upper()] - sets[set_name][constants.SET_LIST_SCRYFALL] = ["VOW","MID"] - counter += 1 - elif (set["set_type"] in constants.SUPPORTED_SET_TYPES): - if set["set_type"] == constants.SET_TYPE_ALCHEMY: + sets[set_name][constants.SET_LIST_ARENA] = ["VOW", "MID"] + sets[set_name][constants.SET_LIST_17LANDS] = [ + set_code.upper()] + sets[set_name][constants.SET_LIST_SCRYFALL] = ["VOW", "MID"] + counter += 1 + elif card_set["set_type"] in constants.SUPPORTED_SET_TYPES: + if card_set["set_type"] == constants.SET_TYPE_ALCHEMY: sets[set_name] = {} - if ("parent_set_code" in set) and ("block_code" in set): - sets[set_name][constants.SET_LIST_ARENA] = [set["parent_set_code"].upper()] - sets[set_name][constants.SET_LIST_17LANDS] = [f"{set['block_code'].upper()}{set['parent_set_code'].upper()}"] - sets[set_name][constants.SET_LIST_SCRYFALL] = [set_code.upper(), set["parent_set_code"].upper()] + if ("parent_set_code" in card_set) and ("block_code" in card_set): + sets[set_name][constants.SET_LIST_ARENA] = [ + card_set["parent_set_code"].upper()] + sets[set_name][constants.SET_LIST_17LANDS] = [ + f"{card_set['block_code'].upper()}{card_set['parent_set_code'].upper()}"] + sets[set_name][constants.SET_LIST_SCRYFALL] = [ + set_code.upper(), card_set["parent_set_code"].upper()] else: - sets[set_name] = {key:[set_code.upper()] for key in [constants.SET_LIST_ARENA, constants.SET_LIST_SCRYFALL, constants.SET_LIST_17LANDS]} - elif (set["set_type"] == constants.SET_TYPE_MASTERS) and (set["digital"] == False): + sets[set_name] = {key: [set_code.upper()] for key in [ + constants.SET_LIST_ARENA, constants.SET_LIST_SCRYFALL, constants.SET_LIST_17LANDS]} + elif (card_set["set_type"] == constants.SET_TYPE_MASTERS) and (not card_set["digital"]): continue else: - sets[set_name] = {key:[set_code.upper()] for key in [constants.SET_LIST_ARENA, constants.SET_LIST_SCRYFALL, constants.SET_LIST_17LANDS]} - #Add mystic archives to strixhaven + sets[set_name] = {key: [set_code.upper()] for key in [ + constants.SET_LIST_ARENA, constants.SET_LIST_SCRYFALL, constants.SET_LIST_17LANDS]} + # Add mystic archives to strixhaven if set_code == "stx": - sets[set_name][constants.SET_LIST_ARENA].append("STA") - sets[set_name][constants.SET_LIST_SCRYFALL].append("STA") + sets[set_name][constants.SET_LIST_ARENA].append( + "STA") + sets[set_name][constants.SET_LIST_SCRYFALL].append( + "STA") counter += 1 - + # Only retrieve the last X sets + CUBE if counter >= constants.SET_LIST_COUNT_MAX: break except Exception as error: - file_logger.info(f"ProcessSetData Error: {error}") - - #Insert the cube sets - sets["Arena Cube"] = {constants.SET_LIST_ARENA : [constants.SET_SELECTION_ALL], constants.SET_LIST_SCRYFALL : [], constants.SET_LIST_17LANDS : [constants.SET_SELECTION_CUBE]} - sets["Arena Cube"][constants.SET_START_DATE] = DateShift(datetime.date.today(), constants.SET_ARENA_CUBE_START_OFFSET_DAYS, "%Y-%m-%d")[1] - - + file_logger.info("_process_set_data Error: %s", error) + + # Insert the cube sets + sets["Arena Cube"] = {constants.SET_LIST_ARENA: [constants.SET_SELECTION_ALL], constants.SET_LIST_SCRYFALL: [ + ], constants.SET_LIST_17LANDS: [constants.SET_SELECTION_CUBE]} + sets["Arena Cube"][constants.SET_START_DATE] = shift_date( + datetime.date.today(), constants.SET_ARENA_CUBE_START_OFFSET_DAYS, "%Y-%m-%d")[1] + return sets - - def ProcessRepositoryVersionData(self, data): + + def _process_repository_version(self, data): + '''Convert a version string to float''' version = round(float(data.decode("ascii")) * 100) - + return version - def ExportData(self): + + def export_card_data(self): + '''Build the file for the set data''' result = True try: - output_file = "_".join((self.selected_sets[constants.SET_LIST_17LANDS][0], self.draft, constants.SET_FILE_SUFFIX)) + output_file = "_".join( + (self.selected_sets[constants.SET_LIST_17LANDS][0], self.draft, constants.SET_FILE_SUFFIX)) location = os.path.join(constants.SETS_FOLDER, output_file) - with open(location, 'w') as f: - json.dump(self.combined_data, f) - - #Verify that the file was written - write_result, write_data = FileIntegrityCheck(location) - - if write_result != Result.VALID: + with open(location, 'w', encoding="utf-8", errors="replace") as file: + json.dump(self.combined_data, file) + + # Verify that the file was written + write_data = check_file_integrity(location) + + if write_data[0] != Result.VALID: result = False - + except Exception as error: - file_logger.info(f"ExportData Error: {error}") + file_logger.info("export_card_data Error: %s", error) result = False - - return result \ No newline at end of file + + return result diff --git a/log_scanner.py b/log_scanner.py index 6a7f8af..56bf587 100644 --- a/log_scanner.py +++ b/log_scanner.py @@ -1,33 +1,36 @@ +"""This module contains the functions that are used for parsing the Arena log""" import os -import time import json import re import logging -import constants +import constants import card_logic as CL import file_extractor as FE -from collections import OrderedDict if not os.path.exists(constants.DRAFT_LOG_FOLDER): os.makedirs(constants.DRAFT_LOG_FOLDER) scanner_logger = logging.getLogger(constants.LOG_TYPE_DEBUG) + class ArenaScanner: + '''Class that handles the processing of the information within Arena Player.log file''' + def __init__(self, filename, step_through, set_list): self.arena_file = filename self.set_list = set_list self.logger = logging.getLogger(constants.LOG_TYPE_DRAFT) self.logger.setLevel(logging.INFO) - + self.logging_enabled = True - + self.step_through = step_through self.set_data = None self.draft_type = constants.LIMITED_TYPE_UNKNOWN self.pick_offset = 0 self.pack_offset = 0 self.search_offset = 0 + self.draft_start_offset = 0 self.draft_sets = [] self.current_pick = 0 self.picked_cards = [[] for i in range(8)] @@ -41,37 +44,44 @@ def __init__(self, filename, step_through, set_list): self.file_size = 0 self.data_source = "None" - def ArenaFile(self, filename): + def set_arena_file(self, filename): + '''Public function that's used for storing the location of the Player.log file''' self.arena_file = filename - def LogEnable(self, enable): + def log_enable(self, enable): + '''Enable/disable the application draft log feature that records draft data in a log file within the Logs folder''' self.logging_enabled = enable - self.LogSuspend(not enable) + self.log_suspend(not enable) - def LogSuspend(self, suspended): + def log_suspend(self, suspended): + '''Prevents the application from updating the draft log file''' if suspended: self.logger.setLevel(logging.CRITICAL) elif self.logging_enabled: self.logger.setLevel(logging.INFO) - def NewLog(self, set, event, id): + def _new_log(self, card_set, event, draft_id): + '''Create a new draft log file''' try: - log_name = f"DraftLog_{set}_{event}_{id}.log" + log_name = f"DraftLog_{card_set}_{event}_{draft_id}.log" log_path = os.path.join(constants.DRAFT_LOG_FOLDER, log_name) for handler in self.logger.handlers: if isinstance(handler, logging.FileHandler): self.logger.removeHandler(handler) - formatter = logging.Formatter('%(asctime)s,%(message)s', datefmt='<%d%m%Y %H:%M:%S>') + formatter = logging.Formatter( + '%(asctime)s,%(message)s', datefmt='<%d%m%Y %H:%M:%S>') new_handler = logging.FileHandler(log_path, delay=True) new_handler.setFormatter(formatter) self.logger.addHandler(new_handler) - scanner_logger.info(f"Creating new draft log: {log_path}") + scanner_logger.info("Creating new draft log: %s", log_path) except Exception as error: - scanner_logger.info(f"NewLog Error: {error}") + scanner_logger.info("_new_log Error: %s", error) - def ClearDraft(self, full_clear): + def clear_draft(self, full_clear): + '''Clear the stored draft data collected from the Player.log file''' if full_clear: self.search_offset = 0 + self.draft_start_offset = 0 self.file_size = 0 self.set_data = None self.draft_type = constants.LIMITED_TYPE_UNKNOWN @@ -88,48 +98,55 @@ def ClearDraft(self, full_clear): self.previous_picked_pack = 0 self.current_picked_pick = 0 self.data_source = "None" - - def DraftStartSearch(self): + + def draft_start_search(self): + '''Search for the string that represents the start of a draft''' update = False event_type = "" event_line = "" draft_id = "" - + try: - #Check if a new player.log was created (e.g. application was started before Arena was started) + # Check if a new player.log was created (e.g. application was started before Arena was started) arena_file_size = os.path.getsize(self.arena_file) if self.file_size > arena_file_size: - self.ClearDraft(True) - scanner_logger.info(f"New Arena log detected ({self.file_size}), ({arena_file_size})") + self.clear_draft(True) + scanner_logger.info( + "New Arena log detected (%d), (%d)", self.file_size, arena_file_size) self.file_size = arena_file_size offset = self.search_offset - with open(self.arena_file, 'r') as log: + with open(self.arena_file, 'r', encoding="utf-8", errors="replace") as log: log.seek(offset) - while(True): + while True: line = log.readline() if not line: break offset = log.tell() + self.search_offset = offset for start_string in constants.DRAFT_START_STRINGS: if start_string in line: - self.search_offset = offset + self.draft_start_offset = offset string_offset = line.find(start_string) - event_data = json.loads(line[string_offset + len(start_string):]) - update, event_type, draft_id = self.DraftStartSearchV1(event_data) + event_data = json.loads( + line[string_offset + len(start_string):]) + update, event_type, draft_id = self.draft_start_search_v1( + event_data) event_line = line - if update: - self.NewLog(self.draft_sets[0], event_type, draft_id) + self._new_log(self.draft_sets[0], event_type, draft_id) self.logger.info(event_line) - self.pick_offset = self.search_offset - self.pack_offset = self.search_offset - scanner_logger.info(f"New draft detected {event_type}, {self.draft_sets}") + self.pick_offset = self.draft_start_offset + self.pack_offset = self.draft_start_offset + scanner_logger.info( + "New draft detected %s, %s", event_type, self.draft_sets) except Exception as error: - scanner_logger.info(f"DraftStartSearch Error: {error}") - + scanner_logger.info("draft_start_search Error: %s", error) + return update - def DraftStartSearchV1(self, event_data): + + def draft_start_search_v1(self, event_data): + '''Parse a draft start string and extract pertinent information''' update = False event_type = "" draft_id = "" @@ -138,62 +155,45 @@ def DraftStartSearchV1(self, event_data): request_data = json.loads(event_data["request"]) payload_data = json.loads(request_data["Payload"]) event_name = payload_data["EventName"] - - scanner_logger.info(f"Event found {event_name}") - + + scanner_logger.info("Event found %s", event_name) + event_sections = event_name.split('_') - - #Find set name within the event string - sets = [i[constants.SET_LIST_17LANDS][0] for i in self.set_list.values() for x in event_sections if i[constants.SET_LIST_17LANDS][0].lower() in x.lower()] - sets = list(dict.fromkeys(sets)) #Remove duplicates while retaining order + + # Find set name within the event string + sets = [i[constants.SET_LIST_17LANDS][0] for i in self.set_list.values( + ) for x in event_sections if i[constants.SET_LIST_17LANDS][0].lower() in x.lower()] + # Remove duplicates while retaining order + sets = list(dict.fromkeys(sets)) events = [] if sets: - #Find event type in event string - events = [i for i in constants.LIMITED_TYPES_DICT.keys() for x in event_sections if i in x] - + # Find event type in event string + events = [i for i in constants.LIMITED_TYPES_DICT + for x in event_sections if i in x] + if not events and [i for i in constants.DRAFT_DETECTION_CATCH_ALL for x in event_sections if i in x]: - events.append(constants.LIMITED_TYPE_STRING_DRAFT_PREMIER) #Unknown draft events will be parsed as premier drafts - + # Unknown draft events will be parsed as premier drafts + events.append(constants.LIMITED_TYPE_STRING_DRAFT_PREMIER) + if sets and events: - #event_set = sets[0] if events[0] == constants.LIMITED_TYPE_STRING_SEALED: - #Trad_Sealed_NEO_20220317 + # Trad_Sealed_NEO_20220317 event_type = constants.LIMITED_TYPE_STRING_TRAD_SEALED if "Trad" in event_sections else constants.LIMITED_TYPE_STRING_SEALED else: event_type = events[0] draft_type = constants.LIMITED_TYPES_DICT[event_type] - self.ClearDraft(False) + self.clear_draft(False) self.draft_type = draft_type self.draft_sets = sets update = True except Exception as error: - scanner_logger.info(f"DraftStartSearchV1 Error: {error}") - + scanner_logger.info("draft_start_search_v1 Error: %s", error) + return update, event_type, draft_id - def DraftStartSearchV2(self, event_data): - try: - request_data = json.loads(event_data["request"]) - params_data = request_data["params"] - event_name = params_data["eventName"] - - event_string = event_name.split('_') - - if len(event_string) > 1: - event_type = event_string[0] - event_set = event_string[1] - - if event_type in constants.LIMITED_TYPES_DICT.keys(): - self.draft_type = constants.LIMITED_TYPES_DICT[event_type] - self.draft_sets = [event_set.upper()] - self.NewLog(event_set, event_type) - self.logger.info(event_data) - - except Exception as error: - scanner_logger.info(f"DraftStartSearchV2 Error: {error}") - - #Wrapper function for performing a search based on the draft type - def DraftDataSearch(self): + + def draft_data_search(self): + '''Collect draft data from the Player.log file based on the current active format''' update = False previous_pick = self.current_pick previous_pack = self.current_pack @@ -201,61 +201,63 @@ def DraftDataSearch(self): if self.draft_type == constants.LIMITED_TYPE_DRAFT_PREMIER_V1: if len(self.initial_pack[0]) == 0: - self.DraftPackSearchPremierP1P1() - self.DraftPackSearchPremierV1() - self.DraftPickedSearchPremierV1() + self._draft_pack_search_premier_p1p1() + self._draft_pack_search_premier_v1() + self._draft_picked_search_premier_v1() elif self.draft_type == constants.LIMITED_TYPE_DRAFT_PREMIER_V2: if len(self.initial_pack[0]) == 0: - self.DraftPackSearchPremierP1P1() - self.DraftPackSearchPremierV2() - self.DraftPickedSearchPremierV2() + self._draft_pack_search_premier_p1p1() + self._draft_pack_search_premier_v2() + self._draft_picked_search_premier_v2() elif self.draft_type == constants.LIMITED_TYPE_DRAFT_QUICK: - self.DraftPackSearchQuick() - self.DraftPickedSearchQuick() + self._draft_pack_search_quick() + self._draft_picked_search_quick() elif self.draft_type == constants.LIMITED_TYPE_DRAFT_TRADITIONAL: if len(self.initial_pack[0]) == 0: - self.DraftPackSearchTraditionalP1P1() - self.DraftPackSearchTraditional() - self.DraftPickedSearchTraditional() - elif (self.draft_type == constants.LIMITED_TYPE_SEALED) or (self.draft_type == constants.LIMITED_TYPE_SEALED_TRADITIONAL): - update = self.SealedPackSearch() + self._draft_pack_search_traditional_p1p1() + self._draft_pack_search_traditional() + self._draft_picked_search_traditional() + elif ((self.draft_type == constants.LIMITED_TYPE_SEALED) + or (self.draft_type == constants.LIMITED_TYPE_SEALED_TRADITIONAL)): + update = self._sealed_pack_search() if not update: - if ((previous_pack != self.current_pack) or + if ((previous_pack != self.current_pack) or (previous_pick != self.current_pick) or - (previous_picked != self.current_picked_pick)): + (previous_picked != self.current_picked_pick)): update = True return update - - def DraftPackSearchPremierP1P1(self): + + def _draft_pack_search_premier_p1p1(self): + '''Parse premier draft string that contains the P1P1 pack data''' offset = self.pack_offset draft_data = object() draft_string = "CardsInPack" pack_cards = [] pack = 0 pick = 0 - #Identify and print out the log lines that contain the draft packs + # Identify and print out the log lines that contain the draft packs try: - with open(self.arena_file, 'r') as log: + with open(self.arena_file, 'r', encoding="utf-8", errors="replace") as log: log.seek(offset) - while(True): + while True: line = log.readline() if not line: break offset = log.tell() - + string_offset = line.find(draft_string) - + if string_offset != -1: - #Remove any prefix (e.g. log timestamp) - start_offset = line.find("{\"id\":") + # Remove any prefix (e.g. log timestamp) + start_offset = line.find("{\"id\":") self.logger.info(line) draft_data = json.loads(line[start_offset:]) request_data = draft_data["request"] payload_data = json.loads(request_data)["Payload"] - + pack_cards = [] try: @@ -264,407 +266,423 @@ def DraftPackSearchPremierP1P1(self): for card in cards: pack_cards.append(str(card)) - + pack = card_data["PackNumber"] pick = card_data["PickNumber"] - + pack_index = (pick - 1) % 8 - + if self.current_pack != pack: self.initial_pack = [[]] * 8 - + if len(self.initial_pack[pack_index]) == 0: self.initial_pack[pack_index] = pack_cards - + self.pack_cards[pack_index] = pack_cards - + if (self.current_pack == 0) and (self.current_pick == 0): self.current_pack = pack self.current_pick = pick - - if(self.step_through): + + if self.step_through: break - + except Exception as error: - self.logger.info(f"DraftPackSearchPremierP1P1 Sub Error: {error}") + self.logger.info( + "_draft_pack_search_premier_p1p1 Sub Error: %s", error) except Exception as error: - self.logger.info(f"DraftPackSearchPremierP1P1 Error: {error}") - + self.logger.info( + "_draft_pack_search_premier_p1p1 Error: %s", error) + return pack_cards - def DraftPickedSearchPremierV1(self): + + def _draft_picked_search_premier_v1(self): + '''Parse the premier draft string that contains the player pick information''' offset = self.pick_offset draft_data = object() draft_string = "[UnityCrossThreadLogger]==> Event_PlayerDraftMakePick " pack = 0 pick = 0 - #Identify and print out the log lines that contain the draft packs + # Identify and print out the log lines that contain the draft packs try: - with open(self.arena_file, 'r') as log: + with open(self.arena_file, 'r', encoding="utf-8", errors="replace") as log: log.seek(offset) - while(True): + while True: line = log.readline() if not line: break offset = log.tell() - + string_offset = line.find(draft_string) - + if string_offset != -1: self.pick_offset = offset start_offset = line.find("{\"id\"") self.logger.info(line) try: - #Identify the pack + # Identify the pack draft_data = json.loads(line[start_offset:]) - + request_data = json.loads(draft_data["request"]) param_data = json.loads(request_data["Payload"]) - + pack = int(param_data["Pack"]) pick = int(param_data["Pick"]) card = str(param_data["GrpId"]) - + pack_index = (pick - 1) % 8 - + if self.previous_picked_pack != pack: self.picked_cards = [[] for i in range(8)] - + self.picked_cards[pack_index].append(card) self.taken_cards.append(card) - + self.previous_picked_pack = pack self.current_picked_pick = pick - + if self.step_through: - break; - + break + except Exception as error: - self.logger.info(f"DraftPickedSearchPremierV1 Error: {error}") + self.logger.info( + "_draft_picked_search_premier_v1 Error: %s", error) except Exception as error: - self.logger.info(f"DraftPickedSearchPremierV1 Error: {error}") + self.logger.info( + "_draft_picked_search_premier_v1 Error: %s", error) - - def DraftPackSearchPremierV1(self): + def _draft_pack_search_premier_v1(self): + '''Parse the premier draft string that contains the non-P1P1 pack data''' offset = self.pack_offset draft_data = object() draft_string = "[UnityCrossThreadLogger]Draft.Notify " pack_cards = [] pack = 0 pick = 0 - #Identify and print out the log lines that contain the draft packs + # Identify and print out the log lines that contain the draft packs try: - with open(self.arena_file, 'r') as log: + with open(self.arena_file, 'r', encoding="utf-8", errors="replace") as log: log.seek(offset) - while(True): + while True: line = log.readline() if not line: break offset = log.tell() - + string_offset = line.find(draft_string) - + if string_offset != -1: self.pack_offset = offset start_offset = line.find("{\"draftId\"") self.logger.info(line) pack_cards = [] - #Identify the pack + # Identify the pack draft_data = json.loads(line[start_offset:]) try: - - cards = draft_data["PackCards"].split(',') - + + cards = draft_data["PackCards"].split(',') + for card in cards: pack_cards.append(str(card)) - + pack = draft_data["SelfPack"] pick = draft_data["SelfPick"] - + pack_index = (pick - 1) % 8 - + if self.current_pack != pack: self.initial_pack = [[]] * 8 - + if len(self.initial_pack[pack_index]) == 0: self.initial_pack[pack_index] = pack_cards - + self.pack_cards[pack_index] = pack_cards - + self.current_pack = pack self.current_pick = pick - - if(self.step_through): + + if self.step_through: break - + except Exception as error: - self.logger.info(f"DraftPackSearchPremierV1 Error: {error}") - + self.logger.info( + "__draft_pack_search_premier_v1 Error: %s", error) + except Exception as error: - self.logger.info(f"DraftPackSearchPremierV1 Error: {error}") + self.logger.info("__draft_pack_search_premier_v1 Error: %s", error) return pack_cards - - def DraftPackSearchPremierV2(self): + + def _draft_pack_search_premier_v2(self): + '''Parse the premier draft string that contains the non-P1P1 pack data''' offset = self.pack_offset draft_data = object() draft_string = "[UnityCrossThreadLogger]Draft.Notify " pack_cards = [] pack = 0 pick = 0 - #Identify and print out the log lines that contain the draft packs + # Identify and print out the log lines that contain the draft packs try: - with open(self.arena_file, 'r') as log: + with open(self.arena_file, 'r', encoding="utf-8", errors="replace") as log: log.seek(offset) - while(True): + while True: line = log.readline() if not line: break offset = log.tell() - + string_offset = line.find(draft_string) - + if string_offset != -1: self.pack_offset = offset self.logger.info(line) pack_cards = [] - #Identify the pack + # Identify the pack draft_data = json.loads(line[len(draft_string):]) try: - cards = draft_data["PackCards"].split(',') - + cards = draft_data["PackCards"].split(',') + for card in cards: pack_cards.append(str(card)) - + pack = draft_data["SelfPack"] pick = draft_data["SelfPick"] - + pack_index = (pick - 1) % 8 - + if self.current_pack != pack: self.initial_pack = [[]] * 8 - + if len(self.initial_pack[pack_index]) == 0: self.initial_pack[pack_index] = pack_cards - + self.pack_cards[pack_index] = pack_cards - + self.current_pack = pack self.current_pick = pick - - if(self.step_through): + + if self.step_through: break - + except Exception as error: - self.logger.info(f"DraftPackSearchPremierV2 Error: {error}") - + self.logger.info( + "__draft_pack_search_premier_v2 Error: %s", error) + except Exception as error: - self.logger.info(f"DraftPackSearchPremierV2 Error: {error}") + self.logger.info("__draft_pack_search_premier_v2 Error: %s", error) return pack_cards - - def DraftPickedSearchPremierV2(self): + def _draft_picked_search_premier_v2(self): + '''Parse the premier draft string that contains the player pick data''' offset = self.pick_offset draft_data = object() draft_string = "[UnityCrossThreadLogger]==> Draft.MakeHumanDraftPick " pack = 0 pick = 0 - #Identify and print out the log lines that contain the draft packs + # Identify and print out the log lines that contain the draft packs try: - with open(self.arena_file, 'r') as log: + with open(self.arena_file, 'r', encoding="utf-8", errors="replace") as log: log.seek(offset) - while(True): + while True: line = log.readline() if not line: break offset = log.tell() - + string_offset = line.find(draft_string) - + if string_offset != -1: self.logger.info(line) self.pick_offset = offset try: - #Identify the pack + # Identify the pack draft_data = json.loads(line[len(draft_string):]) - + request_data = json.loads(draft_data["request"]) param_data = request_data["params"] - + pack = int(param_data["packNumber"]) pick = int(param_data["pickNumber"]) card = param_data["cardId"] - + pack_index = (pick - 1) % 8 - + if self.previous_picked_pack != pack: self.picked_cards = [[] for i in range(8)] - + self.picked_cards[pack_index].append(card) self.taken_cards.append(card) - + self.previous_picked_pack = pack self.current_picked_pick = pick - - + if self.step_through: - break; - + break + except Exception as error: - self.logger.info(f"DraftPickedSearchPremierV2 Error: {error}") - + self.logger.info( + "__draft_picked_search_premier_v2 Error: %s", error) + except Exception as error: - self.logger.info(f"DraftPickedSearchPremierV2 Error: {error}") - - def DraftPackSearchQuick(self): + self.logger.info( + "__draft_picked_search_premier_v2 Error: %s", error) + + def _draft_pack_search_quick(self): + '''Parse the quick draft string that contains the current pack data''' offset = self.pack_offset draft_data = object() draft_string = "DraftPack" pack_cards = [] pack = 0 pick = 0 - #Identify and print out the log lines that contain the draft packs + # Identify and print out the log lines that contain the draft packs try: - with open(self.arena_file, 'r') as log: + with open(self.arena_file, 'r', encoding="utf-8", errors="replace") as log: log.seek(offset) - while(True): + while True: line = log.readline() if not line: break offset = log.tell() - + string_offset = line.find(draft_string) - + if string_offset != -1: self.pack_offset = offset - #Remove any prefix (e.g. log timestamp) - start_offset = line.find("{\"CurrentModule\"") + # Remove any prefix (e.g. log timestamp) + start_offset = line.find("{\"CurrentModule\"") self.logger.info(line) draft_data = json.loads(line[start_offset:]) payload_data = json.loads(draft_data["Payload"]) pack_data = payload_data["DraftPack"] draft_status = payload_data["DraftStatus"] - + if draft_status == "PickNext": pack_cards = [] try: cards = pack_data - + for card in cards: pack_cards.append(str(card)) - + pack = payload_data["PackNumber"] + 1 - pick = payload_data["PickNumber"] + 1 + pick = payload_data["PickNumber"] + 1 pack_index = (pick - 1) % 8 - + if self.current_pack != pack: self.initial_pack = [[]] * 8 - + if len(self.initial_pack[pack_index]) == 0: self.initial_pack[pack_index] = pack_cards - + self.pack_cards[pack_index] = pack_cards - + self.current_pack = pack self.current_pick = pick - - if(self.step_through): + + if self.step_through: break - + except Exception as error: - self.logger.info(f"DraftPackSearchQuick Error: {error}") + self.logger.info( + "__draft_pack_search_quick Error: %s", error) except Exception as error: - self.logger.info(f"DraftPackSearchQuick Error: {error}") - + self.logger.info("__draft_pack_search_quick Error: %s", error) + return pack_cards - - def DraftPickedSearchQuick(self): + + def _draft_picked_search_quick(self): + '''Parse the quick draft string that contains the player pick data''' offset = self.pick_offset draft_data = object() draft_string = "[UnityCrossThreadLogger]==> BotDraft_DraftPick " pack = 0 pick = 0 - #Identify and print out the log lines that contain the draft packs + # Identify and print out the log lines that contain the draft packs try: - with open(self.arena_file, 'r') as log: + with open(self.arena_file, 'r', encoding="utf-8", errors="replace") as log: log.seek(offset) - while(True): + while True: line = log.readline() if not line: break offset = log.tell() - + string_offset = line.find(draft_string) - + if string_offset != -1: self.logger.info(line) self.pick_offset = offset try: - #Identify the pack - draft_data = json.loads(line[string_offset+len(draft_string):]) - + # Identify the pack + draft_data = json.loads( + line[string_offset+len(draft_string):]) + request_data = json.loads(draft_data["request"]) payload_data = json.loads(request_data["Payload"]) pick_data = payload_data["PickInfo"] - + pack = pick_data["PackNumber"] + 1 pick = pick_data["PickNumber"] + 1 card = pick_data["CardId"] - + pack_index = (pick - 1) % 8 - + if self.previous_picked_pack != pack: self.picked_cards = [[] for i in range(8)] - + self.previous_picked_pack = pack self.current_picked_pick = pick - + self.picked_cards[pack_index].append(card) self.taken_cards.append(card) - + if self.step_through: break - + except Exception as error: - self.logger.info(f"DraftPickedSearchQuick Error: {error}") + self.logger.info( + "__draft_picked_search_quick Error: %s", error) except Exception as error: - self.logger.info(f"DraftPickedSearchQuick Error: {error}") - - def DraftPackSearchTraditionalP1P1(self): + self.logger.info("__draft_picked_search_quick Error: %s", error) + + def _draft_pack_search_traditional_p1p1(self): + '''Parse the traditional draft string that contains the P1P1 pack data''' offset = self.pack_offset draft_data = object() draft_string = "CardsInPack" pack_cards = [] pack = 0 pick = 0 - #Identify and print out the log lines that contain the draft packs + # Identify and print out the log lines that contain the draft packs try: - with open(self.arena_file, 'r') as log: + with open(self.arena_file, 'r', encoding="utf-8", errors="replace") as log: log.seek(offset) - while(True): + while True: line = log.readline() if not line: break offset = log.tell() - + string_offset = line.find(draft_string) - + if string_offset != -1: - #Remove any prefix (e.g. log timestamp) - start_offset = line.find("{\"id\":") + # Remove any prefix (e.g. log timestamp) + start_offset = line.find("{\"id\":") self.logger.info(line) draft_data = json.loads(line[start_offset:]) request_data = draft_data["request"] payload_data = json.loads(request_data)["Payload"] - + pack_cards = [] try: @@ -673,173 +691,181 @@ def DraftPackSearchTraditionalP1P1(self): for card in cards: pack_cards.append(str(card)) - + pack = card_data["PackNumber"] pick = card_data["PickNumber"] - + pack_index = (pick - 1) % 8 - + if self.current_pack != pack: self.initial_pack = [[]] * 8 - + if len(self.initial_pack[pack_index]) == 0: self.initial_pack[pack_index] = pack_cards - + self.pack_cards[pack_index] = pack_cards - + if (self.current_pack == 0) and (self.current_pick == 0): self.current_pack = pack self.current_pick = pick - - if(self.step_through): + + if self.step_through: break - + except Exception as error: - self.logger.info(f"DraftPackSearchTraditionalP1P1 Error: {error}") + self.logger.info( + "__draft_pack_search_traditional_p1p1 Error: %s", error) except Exception as error: - self.logger.info(f"DraftPackSearchTraditionalP1P1 Error: {error}") - + self.logger.info( + "__draft_pack_search_traditional_p1p1 Error: %s", error) + return pack_cards - - def DraftPickedSearchTraditional(self): + + def _draft_picked_search_traditional(self): + '''Parse the traditional draft string that contains the player pick data''' offset = self.pick_offset draft_data = object() draft_string = "[UnityCrossThreadLogger]==> Event_PlayerDraftMakePick " pack = 0 pick = 0 - #Identify and print out the log lines that contain the draft packs + # Identify and print out the log lines that contain the draft packs try: - with open(self.arena_file, 'r') as log: + with open(self.arena_file, 'r', encoding="utf-8", errors="replace") as log: log.seek(offset) - while(True): + while True: line = log.readline() if not line: break offset = log.tell() - + string_offset = line.find(draft_string) - + if string_offset != -1: self.pick_offset = offset start_offset = line.find("{\"id\"") self.logger.info(line) try: - #Identify the pack + # Identify the pack draft_data = json.loads(line[start_offset:]) - + request_data = json.loads(draft_data["request"]) param_data = json.loads(request_data["Payload"]) - + pack = int(param_data["Pack"]) pick = int(param_data["Pick"]) card = str(param_data["GrpId"]) - + pack_index = (pick - 1) % 8 - + if self.previous_picked_pack != pack: self.picked_cards = [[] for i in range(8)] - + self.picked_cards[pack_index].append(card) self.taken_cards.append(card) - + self.previous_picked_pack = pack self.current_picked_pick = pick - + if self.step_through: - break; - + break + except Exception as error: - self.logger.info(f"DraftPickedSearchTraditional Error: {error}") + self.logger.info( + "__draft_picked_search_traditional Error: %s", error) except Exception as error: - self.logger.info(f"DraftPickedSearchTraditional Error: {error}") + self.logger.info( + "__draft_picked_search_traditional Error: %s", error) - - def DraftPackSearchTraditional(self): + def _draft_pack_search_traditional(self): + '''Parse the quick draft string that contains the non-P1P1 pack data''' offset = self.pack_offset draft_data = object() draft_string = "[UnityCrossThreadLogger]Draft.Notify " pack_cards = [] pack = 0 pick = 0 - #Identify and print out the log lines that contain the draft packs + # Identify and print out the log lines that contain the draft packs try: - with open(self.arena_file, 'r') as log: + with open(self.arena_file, 'r', encoding="utf-8", errors="replace") as log: log.seek(offset) - while(True): + while True: line = log.readline() if not line: break offset = log.tell() - + string_offset = line.find(draft_string) - + if string_offset != -1: self.pack_offset = offset start_offset = line.find("{\"draftId\"") self.logger.info(line) pack_cards = [] - #Identify the pack + # Identify the pack draft_data = json.loads(line[start_offset:]) try: - - cards = draft_data["PackCards"].split(',') - + + cards = draft_data["PackCards"].split(',') + for card in cards: pack_cards.append(str(card)) - + pack = draft_data["SelfPack"] pick = draft_data["SelfPick"] - + pack_index = (pick - 1) % 8 - + if self.current_pack != pack: self.initial_pack = [[]] * 8 - + if len(self.initial_pack[pack_index]) == 0: self.initial_pack[pack_index] = pack_cards - + self.pack_cards[pack_index] = pack_cards - + self.current_pack = pack self.current_pick = pick - - if(self.step_through): + + if self.step_through: break - + except Exception as error: - self.logger.info(f"DraftPackSearchTraditional Error: {error}") - + self.logger.info( + "__draft_pack_search_traditional Error: %s", error) + except Exception as error: - self.logger.info(f"DraftPackSearchTraditional Error: {error}") + self.logger.info( + "__draft_pack_search_traditional Error: %s", error) return pack_cards - def SealedPackSearch(self): + def _sealed_pack_search(self): + '''Parse sealed string that contains all of the card data''' offset = self.pack_offset draft_data = object() draft_string = "EventGrantCardPool" update = False - #Identify and print out the log lines that contain the draft packs + # Identify and print out the log lines that contain the draft packs try: - with open(self.arena_file, 'r') as log: + with open(self.arena_file, 'r', encoding="utf-8", errors="replace") as log: log.seek(offset) - while(True): + while True: line = log.readline() if not line: break offset = log.tell() - + string_offset = line.find(draft_string) - + if string_offset != -1: update = True self.pack_offset = offset start_offset = line.find("{\"CurrentModule\"") self.logger.info(line) - #Identify the pack + # Identify the pack draft_data = json.loads(line[start_offset:]) payload_data = json.loads(draft_data["Payload"]) changes = payload_data["Changes"] @@ -850,186 +876,211 @@ def SealedPackSearch(self): for card_data in card_list_data: card = str(card_data["GrpId"]) self.taken_cards.append(card) - + except Exception as error: - self.logger.info(f"SealedPackSearch Error: {error}") - + self.logger.info( + "__sealed_pack_search Error: %s", error) + except Exception as error: - self.logger.info(f"SealedPackSearch Error: {error}") + self.logger.info("__sealed_pack_search Error: %s", error) return update - - def RetrieveDataSources(self): - data_sources = OrderedDict() - + + def retrieve_data_sources(self): + '''Return a list of set files that can be used with the current active draft''' + data_sources = {} + try: if self.draft_type != constants.LIMITED_TYPE_UNKNOWN: draft_list = list(constants.LIMITED_TYPES_DICT.keys()) - draft_type = [x for x in draft_list if constants.LIMITED_TYPES_DICT[x] == self.draft_type][0] - draft_list.insert(0, draft_list.pop(draft_list.index(draft_type))) - - #Search for the set files - for set in self.draft_sets: - for type in draft_list: - file_name = "_".join((set,type,constants.SET_FILE_SUFFIX)) - file = FE.SearchLocalFiles([constants.SETS_FOLDER], [file_name]) - if len(file): - result, json_data = FE.FileIntegrityCheck(file[0]) - - if result == FE.Result.VALID: - type_string = f"[{set[0:3]}]{type}" if re.findall("^[Yy]\d{2}", set) else type + draft_type = [ + x for x in draft_list if constants.LIMITED_TYPES_DICT[x] == self.draft_type][0] + draft_list.insert(0, draft_list.pop( + draft_list.index(draft_type))) + + # Search for the set files + for draft_set in self.draft_sets: + for draft_type in draft_list: + file_name = "_".join( + (draft_set, draft_type, constants.SET_FILE_SUFFIX)) + file = FE.search_local_files( + [constants.SETS_FOLDER], [file_name]) + if file: + result = FE.check_file_integrity(file[0]) + + if result[0] == FE.Result.VALID: + type_string = f"[{draft_set[0:3]}]{draft_type}" if re.findall( + "^[Yy]\d{2}", draft_set) else draft_type data_sources[type_string] = file[0] - + except Exception as error: - scanner_logger.info(f"RetrieveDataSources Error: {error}") - - if len(data_sources) == 0: + scanner_logger.info("retrieve_data_sources Error: %s", error) + + if not data_sources: data_sources = constants.DATA_SOURCES_NONE return data_sources - - def RetrieveTierSource(self): + + def retrieve_tier_source(self): + '''Return a list of tier files that can be used with the current active draft''' tier_sources = [] - + try: if self.draft_sets: - file = FE.SearchLocalFiles([constants.TIER_FOLDER], [constants.TIER_FILE_PREFIX]) - - if len(file): + file = FE.search_local_files([constants.TIER_FOLDER], [ + constants.TIER_FILE_PREFIX]) + + if file: tier_sources = file - + except Exception as error: - scanner_logger.info(f"RetrieveTierSource Error: {error}") + scanner_logger.info("retrieve_tier_source Error: %s", error) return tier_sources - - def RetrieveSetData(self, file): + + def retrieve_set_data(self, file): + '''Retrieve set data from the set data files''' result = FE.Result.ERROR_MISSING_FILE self.set_data = None - + try: - result, json_data = FE.FileIntegrityCheck(file) - + result, json_data = FE.check_file_integrity(file) + if result == FE.Result.VALID: self.set_data = json_data - + except Exception as error: - scanner_logger.info(f"RetrieveSetData Error: {error}") - + scanner_logger.info("retrieve_set_data Error: %s", error) + return result - - def RetrieveSetMetrics(self, bayesian_enabled): - set_metrics = {"mean": 0, "standard_deviation" : 0} - + + def retrieve_set_metrics(self, bayesian_enabled): + '''Parse set data and calculate the mean and standard deviation for a set''' + set_metrics = CL.SetMetrics() + try: if self.set_data: - mean = CL.CalculateMean(self.set_data["card_ratings"], bayesian_enabled) - standard_deviation = CL.CalculateStandardDeviation(self.set_data["card_ratings"], mean, bayesian_enabled) - set_metrics = {"mean": mean, "standard_deviation" : standard_deviation} + set_metrics.mean = CL.calculate_mean( + self.set_data["card_ratings"], bayesian_enabled) + set_metrics.standard_deviation = CL.calculate_standard_deviation( + self.set_data["card_ratings"], set_metrics.mean, bayesian_enabled) #print(f"Mean:{mean}, Standard Deviation: {standard_deviation}") except Exception as error: - scanner_logger.info(f"RetrieveSetMetrics Error: {error}") + scanner_logger.info("retrieve_set_metrics Error: %s", error) return set_metrics - - def RetrieveColorWinRate(self, label_type): + + def retrieve_color_win_rate(self, label_type): + '''Parse set data and return a list of color win rates''' deck_colors = {} for colors in constants.DECK_FILTERS: deck_color = colors if (label_type == constants.DECK_FILTER_FORMAT_NAMES) and (deck_color in constants.COLOR_NAMES_DICT): deck_color = constants.COLOR_NAMES_DICT[deck_color] deck_colors[colors] = deck_color - + try: if self.set_data: - for colors in self.set_data["color_ratings"].keys(): - for deck_color in deck_colors.keys(): + for colors in self.set_data["color_ratings"]: + for deck_color in deck_colors: if (len(deck_color) == len(colors)) and set(deck_color).issubset(colors): filter_label = deck_color if (label_type == constants.DECK_FILTER_FORMAT_NAMES) and (deck_color in constants.COLOR_NAMES_DICT): filter_label = constants.COLOR_NAMES_DICT[deck_color] - ratings_string = filter_label + " (%s%%)" % (self.set_data["color_ratings"][colors]) + ratings_string = filter_label + \ + f' ({self.set_data["color_ratings"][colors]}%)' deck_colors[deck_color] = ratings_string except Exception as error: - scanner_logger.info(f"RetrieveColorWinRate Error: {error}") + scanner_logger.info("retrieve_color_win_rate Error: %s", error) - #Switch key and value + # Switch key and value deck_colors = {v: k for k, v in deck_colors.items()} return deck_colors - - def PickedCards(self, pack_index): + + def retrieve_picked_cards(self, pack_index): + '''Return the card data for the card that was picked from a from a specific pack''' picked_cards = [] - - if self.set_data != None: + + if self.set_data is not None: if pack_index < len(self.picked_cards): for card in self.picked_cards[pack_index]: try: - picked_cards.append(self.set_data["card_ratings"][card][constants.DATA_FIELD_NAME]) + picked_cards.append( + self.set_data["card_ratings"][card][constants.DATA_FIELD_NAME]) except Exception as error: - scanner_logger.info(f"PickedCards Error: {error}") - - return picked_cards + scanner_logger.info( + "retrieve_picked_cards Error: %s", error) + + return picked_cards - def InitialPackCards(self, pack_index): + def retrieve_initial_pack_cards(self, pack_index): + '''Return the card data for a list of cards from a specific pack''' pack_cards = [] - - if self.set_data != None: + + if self.set_data is not None: if pack_index < len(self.initial_pack): for card in self.initial_pack[pack_index]: try: pack_cards.append(self.set_data["card_ratings"][card]) except Exception as error: - scanner_logger.info(f"InitialPackCards Error: {error}") - - return pack_cards - - def PackCards(self, pack_index): + scanner_logger.info( + "retrieve_initial_pack_cards Error: %s", error) + + return pack_cards + + def retrieve_pack_cards(self, pack_index): + '''Return the card data for a list of cards from a specific pack''' pack_cards = [] - - if self.set_data != None: + + if self.set_data is not None: if pack_index < len(self.pack_cards): for card in self.pack_cards[pack_index]: try: pack_cards.append(self.set_data["card_ratings"][card]) except Exception as error: - scanner_logger.info(f"PackCards Error: {error}") - + scanner_logger.info( + "retrieve_pack_cards Error: %s", error) + return pack_cards - - def TakenCards(self): + + def retrieve_taken_cards(self): + '''Return the card data for all of the cards that were picked during the draft''' taken_cards = [] - - if self.set_data != None: + + if self.set_data is not None: for card in self.taken_cards: try: taken_cards.append(self.set_data["card_ratings"][card]) except Exception as error: - scanner_logger.info(f"TakenCards Error: {error}") - + scanner_logger.info( + "retrieve_taken_cards Error: %s", error) + return taken_cards - - def RetrieveTierData(self, files): + + def retrieve_tier_data(self, files): + '''Parse a tier list file and return the tier data''' tier_data = {} tier_options = {} count = 0 try: for file in files: if os.path.exists(file): - with open(file, 'r') as json_file: + with open(file, 'r', encoding="utf-8", errors="replace") as json_file: data = json.loads(json_file.read()) if [i for i in self.draft_sets if i in data["meta"]["set"]]: tier_id = f"Tier{count}" tier_label = data["meta"]["label"] - tier_label = f'{tier_label[:8]}...' if len(tier_label) > 11 else tier_label #Truncate label if it's too long + tier_label = f'{tier_label[:8]}...' if len( + tier_label) > 11 else tier_label # Truncate label if it's too long tier_key = f'{tier_id} ({tier_label})' tier_options[tier_key] = tier_id if data["meta"]["version"] == 1: for card_name, card_rating in data["ratings"].items(): - data["ratings"][card_name] = CL.FormatTierResults(card_rating, - constants.RESULT_FORMAT_RATING, - constants.RESULT_FORMAT_GRADE) + data["ratings"][card_name] = CL.format_tier_results(card_rating, + constants.RESULT_FORMAT_RATING, + constants.RESULT_FORMAT_GRADE) tier_data[tier_id] = data count += 1 - + except Exception as error: - scanner_logger.info(f"RetrieveTierData Error: {error}") - return tier_data, tier_options \ No newline at end of file + scanner_logger.info("retrieve_tier_data Error: %s", error) + return tier_data, tier_options diff --git a/main.py b/main.py index cd4460f..7f8c79c 100644 --- a/main.py +++ b/main.py @@ -1,89 +1,30 @@ #!/usr/bin/env python3 """! @brief Magic the Gathering draft application that utilizes 17Lands data""" - -## -# @mainpage Magic Draft Application -# -# @section description_main Description -# A program that utilizes 17Lands data to dispay pick ratings, deck statistics, and deck suggestions -# -# @section notes_main Notes -# - -# - - -## -# @file main.py -# -# @brief -# -# @section Description -# A program that utilizes 17Lands data to dispay pick ratings, deck statistics, and deck suggestions -# -# @section libraries_main Libraries/Modules -# - tkinter standard library (https://docs.python.org/3/library/tkinter.html) -# - Access to GUI functions. -# - pynput library (https://pypi.org/project/pynput) -# - Access to the keypress monitoring functions. -# - datetime standard library (https://docs.python.org/3/library/datetime.html) -# - Access to the current date function. -# - urllib standard library (https://docs.python.org/3/library/urllib.html) -# - Access to URL opening function. -# - json standard library (https://docs.python.org/3/library/json.html) -# - Access to the json encoding and decoding functions -# - os standard library (https://docs.python.org/3/library/os.html) -# - Access to the file system navigation functions. -# - time standard library (https://docs.python.org/3/library/time.html) -# - Access to sleep function. -# - sys standard library (https://docs.python.org/3/library/sys.html) -# - Access to the command line argument list. -# - io standard library (https://docs.python.org/3/library/sys.html) -# - Access to the command line argument list. -# - PIL library (https://pillow.readthedocs.io/en/stable/) -# - Access to image manipulation modules. -# - ttkwidgets library (https://github.com/TkinterEP/ttkwidgets) -# - Access to the autocomplete entry box widget. -# - file_extractor module (local) -# - Access to the functions used for downloading the data sets. -# - card_logic module (local) -# - Access to the functions used for processing the card data. -# - log_scanner module (local) -# - Access to the functions used for reading the arena log. -# -# @section Notes -# - Comments are Doxygen compatible. -# -# @section TODO -# - None. -# -# @section Author(s) -# - Created by Bryan Stapleton on 12/25/2021 - # Imports import argparse -import sys import overlay as OL -__version__= 3.02 - -def Startup(argv): +__version__ = 3.03 + + +def startup(): parser = argparse.ArgumentParser() - - parser.add_argument('-f','--file') - parser.add_argument('-d','--data') + + parser.add_argument('-f', '--file') + parser.add_argument('-d', '--data') parser.add_argument('--step', action='store_true') - + args = parser.parse_args() - - overlay = OL.Overlay(__version__, args) - - overlay.MainLoop() - - -def main(argv): - Startup(argv) - + + overlay = OL.Overlay(__version__, args) + + overlay.main_loop() + + +def main(): + startup() + + if __name__ == "__main__": - main(sys.argv[1:]) - \ No newline at end of file + main() diff --git a/overlay.py b/overlay.py index f2638d1..c9674c8 100644 --- a/overlay.py +++ b/overlay.py @@ -1,20 +1,22 @@ -from tkinter import * -from tkinter.ttk import * +"""This module contains the functions and classes that are used for building and handling the application UI""" +import tkinter +from tkinter.ttk import Progressbar, Treeview, Style, OptionMenu, Button, Checkbutton, Label from tkinter import filedialog from datetime import date -from pynput.keyboard import Key, Listener, KeyCode -import tkinter.messagebox as MessageBox import urllib import os import sys import io import logging import logging.handlers -import constants +import math +from ttkwidgets.autocomplete import AutocompleteEntry +from pynput.keyboard import Listener, KeyCode +from PIL import Image, ImageTk import file_extractor as FE import card_logic as CL import log_scanner as LS -from ttkwidgets.autocomplete import AutocompleteEntry +import constants if not os.path.exists(constants.DEBUG_LOG_FOLDER): os.makedirs(constants.DEBUG_LOG_FOLDER) @@ -22,293 +24,357 @@ overlay_logger = logging.getLogger(constants.LOG_TYPE_DEBUG) overlay_logger.setLevel(logging.INFO) handlers = { - logging.handlers.TimedRotatingFileHandler(constants.DEBUG_LOG_FILE, when='D', interval=1, backupCount=7, utc=True), + logging.handlers.TimedRotatingFileHandler( + constants.DEBUG_LOG_FILE, when='D', interval=1, backupCount=7, utc=True), logging.StreamHandler(sys.stdout), } -formatter = logging.Formatter('%(asctime)s,%(message)s', datefmt='<%m/%d/%Y %H:%M:%S>') +formatter = logging.Formatter( + '%(asctime)s,%(message)s', datefmt='<%m/%d/%Y %H:%M:%S>') for handler in handlers: handler.setFormatter(formatter) overlay_logger.addHandler(handler) - - -def CheckVersion(platform, version): + + +def check_version(platform, version): + """Compare the application version and the latest version in the repository""" return_value = False - repository_version = platform.SessionRepositoryVersion() + repository_version = platform.retrieve_repository_version() repository_version = int(repository_version) client_version = round(float(version) * 100) if repository_version > client_version: return_value = True - - + repository_version = round(float(repository_version) / 100.0, 2) return return_value, repository_version -def FixedMap(style, option): - # Returns the style map for 'option' with any styles starting with - # ("!disabled", "!selected", ...) filtered out - # style.map() returns an empty list for missing options, so this should - # be future-safe +def fixed_map(style, option): + ''' Returns the style map for 'option' with any styles starting with + ("!disabled", "!selected", ...) filtered out + style.map() returns an empty list for missing options, so this should + be future-safe''' return [elm for elm in style.map("Treeview", query_opt=option) if elm[:2] != ("!disabled", "!selected")] -def TableColumnControl(table, column_fields): + +def control_table_column(table, column_fields): + """Hide disabled table columns""" visible_columns = [] last_field_index = 0 for count, (key, value) in enumerate(column_fields.items()): if value != constants.DATA_FIELD_DISABLED: - table.heading(key, text = value.upper()) + table.heading(key, text=value.upper()) visible_columns.append(key) last_field_index = count table["displaycolumns"] = visible_columns return last_field_index -def CopySuggested(deck_colors, deck, set_data, color_options): + + +def copy_suggested(deck_colors, deck, color_options): + """Copy the deck and sideboard list from the Suggest Deck window""" colors = color_options[deck_colors.get()] deck_string = "" try: - deck_string = CL.CopyDeck(deck[colors]["deck_cards"],deck[colors]["sideboard_cards"],set_data["card_ratings"]) - CopyClipboard(deck_string) + deck_string = CL.copy_deck( + deck[colors]["deck_cards"], deck[colors]["sideboard_cards"]) + copy_clipboard(deck_string) except Exception as error: - overlay_logger.info(f"CopySuggested Error: {error}") - return - -def CopyTaken(taken_cards, set_data): + overlay_logger.info("copy_suggested Error: %s", error) + return + + +def copy_taken(taken_cards): + """Copy the card list from the Taken Cards window""" deck_string = "" try: - stacked_cards = CL.StackCards(taken_cards) - deck_string = CL.CopyDeck(stacked_cards, None, set_data["card_ratings"]) - CopyClipboard(deck_string) + stacked_cards = CL.stack_cards(taken_cards) + deck_string = CL.copy_deck( + stacked_cards, None) + copy_clipboard(deck_string) except Exception as error: - overlay_logger.info(f"CopyTaken Error: {error}") - return - -def CopyClipboard(copy): + overlay_logger.info("copy_taken Error: %s", error) + return + + +def copy_clipboard(copy): + """Send the copied data to the clipboard""" try: - #Attempt to copy to clipboard - clip = Tk() + # Attempt to copy to clipboard + clip = tkinter.Tk() clip.withdraw() clip.clipboard_clear() clip.clipboard_append(copy) clip.update() clip.destroy() except Exception as error: - overlay_logger.info(f"CopyClipboard Error: {error}") - return - -def CreateHeader(frame, height, font, headers, total_width, include_header, fixed_width, table_style, stretch_enabled): + overlay_logger.info("copy_clipboard Error: %s", error) + return + + +def create_header(frame, height, font, headers, total_width, include_header, fixed_width, table_style, stretch_enabled): + """Configure the tkinter Treeview widget tables that are used to list draft data""" header_labels = tuple(headers.keys()) show_header = "headings" if include_header else "" - column_stretch = YES if stretch_enabled else NO - list_box = Treeview(frame, columns = header_labels, show = show_header, style = table_style) - list_box.config(height=height) + column_stretch = tkinter.YES if stretch_enabled else tkinter.NO + list_box = Treeview(frame, columns=header_labels, + show=show_header, style=table_style, height=height) try: - for k, v in constants.ROW_TAGS_BW_DICT.items(): - list_box.tag_configure(k, font=(v[0],font, "bold"), background=v[1], foreground=v[2]) - - for k, v in constants.ROW_TAGS_COLORS_DICT.items(): - list_box.tag_configure(k, font=(v[0],font, "bold"), background=v[1], foreground=v[2]) + for key, value in constants.ROW_TAGS_BW_DICT.items(): + list_box.tag_configure( + key, font=(value[0], font, "bold"), background=value[1], foreground=value[2]) + + for key, value in constants.ROW_TAGS_COLORS_DICT.items(): + list_box.tag_configure( + key, font=(value[0], font, "bold"), background=value[1], foreground=value[2]) for column in header_labels: if fixed_width: - list_box.column(column, stretch = column_stretch, anchor = headers[column]["anchor"], width = int(headers[column]["width"] * total_width)) + column_width = int( + math.ceil(headers[column]["width"] * total_width)) + list_box.column(column, + stretch=column_stretch, + anchor=headers[column]["anchor"], + width=column_width) else: - list_box.column(column, stretch = column_stretch, anchor = headers[column]["anchor"]) - list_box.heading(column, text = column, anchor = CENTER, command=lambda _col=column: TableColumnSort(list_box, _col, True)) + list_box.column(column, stretch=column_stretch, + anchor=headers[column]["anchor"]) + list_box.heading(column, text=column, anchor=tkinter.CENTER, + command=lambda _col=column: sort_table_column(list_box, _col, True)) list_box["show"] = show_header # use after setting column's size except Exception as error: - overlay_logger.info(f"CreateHeader Error: {error}") + overlay_logger.info("create_header Error: %s", error) return list_box - -def TableRowTag(colors_enabled, colors, index): + + +def identify_table_row_tag(colors_enabled, colors, index): + """Return the row color (black/white or card color) depending on the application settings""" tag = "" - + if colors_enabled: - tag = CL.RowColorTag(colors) + tag = CL.row_color_tag(colors) else: tag = constants.BW_ROW_COLOR_ODD_TAG if index % 2 else constants.BW_ROW_COLOR_EVEN_TAG - + return tag - -def TableColumnSort(table, column, reverse): + + +def sort_table_column(table, column, reverse): + """Sort the table columns when clicked""" row_colors = False - + try: - row_list = [(float(table.set(k, column)), k) for k in table.get_children('')] + # Sort column that contains numeric values + row_list = [(float(table.set(k, column)), k) + for k in table.get_children('')] except ValueError: + # Sort column that contains string values row_list = [(table.set(k, column), k) for k in table.get_children('')] - row_list.sort(key=lambda x: CL.FieldProcessSort(x[0]), reverse=reverse) + row_list.sort(key=lambda x: CL.field_process_sort(x[0]), reverse=reverse) - if len(row_list): + if row_list: tags = table.item(row_list[0][1])["tags"][0] - row_colors = True if tags in constants.ROW_TAGS_COLORS_DICT.keys() else False - - for index, (value, k) in enumerate(row_list): - table.move(k, "", index) - + row_colors = True if tags in constants.ROW_TAGS_COLORS_DICT else False + + for index, value in enumerate(row_list): + table.move(value[1], "", index) + + # Reset the black/white shades for sorted rows if not row_colors: - row_tag = TableRowTag(False, "", index) - table.item(k, tags=row_tag) - - table.heading(column, command=lambda: TableColumnSort(table, column, not reverse)) - -def SafeCoordinates(root, window_width, window_height, offset_x, offset_y): - x = 0 - y = 0 - + row_tag = identify_table_row_tag(False, "", index) + table.item(value[1], tags=row_tag) + + table.heading(column, command=lambda: sort_table_column( + table, column, not reverse)) + + +def identify_safe_coordinates(root, window_width, window_height, offset_x, offset_y): + '''Return x,y coordinates that fall within the bounds of the screen''' + location_x = 0 + location_y = 0 + try: pointer_x = root.winfo_pointerx() pointer_y = root.winfo_pointery() screen_width = root.winfo_screenwidth() screen_height = root.winfo_screenheight() - + if pointer_x + offset_x + window_width > screen_width: - x = max(pointer_x - offset_x - window_width, 0) + location_x = max(pointer_x - offset_x - window_width, 0) else: - x = pointer_x + offset_x - + location_x = pointer_x + offset_x + if pointer_y + offset_y + window_height > screen_height: - y = max(pointer_y - offset_y - window_height, 0) + location_y = max(pointer_y - offset_y - window_height, 0) else: - y = pointer_y + offset_y - + location_y = pointer_y + offset_y + except Exception as error: - overlay_logger.info(f"SafeCoordinates Error: {error}") - - return x, y + overlay_logger.info("identify_safe_coordinates Error: %s", error) + + return location_x, location_y + + +class ScaledWindow: + def __init__(self): + self.scale_factor = 1 + self.fonts_dict = {} + + def _scale_value(self, value): + scaled_value = int(value * self.scale_factor) + + return scaled_value + + +class Overlay(ScaledWindow): + '''Class that handles all of the UI widgets''' -class Overlay: def __init__(self, version, args): - self.root = Tk() + super().__init__() + self.root = tkinter.Tk() self.version = version - self.root.title("Magic Draft %.2f" % version) - self.root.tk.call("source", "dark_mode.tcl") - self.configuration = CL.ReadConfig() + self.root.title(f"Magic Draft {version}") + self.configuration = CL.read_config() + self.root.resizable(False, False) + + self._set_os_configuration() + + self.configuration.table_width = self._scale_value( + self.configuration.table_width) + self.listener = None - + if args.file is None: - self.arena_file = FE.ArenaLogLocation() + self.arena_file = FE.retrieve_arena_log_location() else: self.arena_file = args.file - overlay_logger.info(f"Player Log Location: {self.arena_file}") - + overlay_logger.info("Player Log Location: %s", self.arena_file) + if args.data is None: - self.data_file = FE.ArenaDirectoryLocation(self.arena_file) + self.data_file = FE.retrieve_arena_directory(self.arena_file) else: self.data_file = args.file - overlay_logger.info(f"Card Data Location: {self.data_file}") - + overlay_logger.info("Card Data Location: %s", self.data_file) + self.step_through = args.step - - platform = sys.platform - if platform == constants.PLATFORM_ID_OSX: - self.configuration.hotkey_enabled = False - self.root.resizable(width = True, height = True) - else: - self.root.resizable(False, False) - overlay_logger.info(f"Platform: {platform}") - + self.extractor = FE.FileExtractor(self.data_file) - self.draft = LS.ArenaScanner(self.arena_file, self.step_through, self.extractor.SetList()) - + self.draft = LS.ArenaScanner( + self.arena_file, self.step_through, self.extractor.return_set_list()) + self.trace_ids = [] - + self.tier_data = {} self.main_options_dict = constants.COLUMNS_OPTIONS_MAIN_DICT.copy() self.extra_options_dict = constants.COLUMNS_OPTIONS_EXTRA_DICT.copy() - self.deck_colors = self.draft.RetrieveColorWinRate(self.configuration.filter_format) - self.data_sources = self.draft.RetrieveDataSources() - self.tier_sources = self.draft.RetrieveTierSource() - self.set_metrics = self.draft.RetrieveSetMetrics(False) - - #Grid.rowconfigure(self.root, 9, weight = 1) - Grid.columnconfigure(self.root, 0, weight = 1) - Grid.columnconfigure(self.root, 1, weight = 1) - #Menu Bar - self.menubar = Menu(self.root) - self.filemenu = Menu(self.menubar, tearoff=0) - self.filemenu.add_command(label="Open", command=self.FileOpen) - self.datamenu = Menu(self.menubar, tearoff=0) - self.datamenu.add_command(label="View Sets", command=self.SetViewPopup) - - self.cardmenu = Menu(self.menubar, tearoff=0) - self.cardmenu.add_command(label="Taken Cards", command=self.TakenCardsPopup) - self.cardmenu.add_command(label="Suggest Decks", command=self.SuggestDeckPopup) - self.cardmenu.add_command(label="Compare Cards", command=self.CardComparePopup) - - self.settingsmenu = Menu(self.menubar, tearoff=0) - self.settingsmenu.add_command(label="Settings", command=self.SettingsPopup) - + self.deck_colors = self.draft.retrieve_color_win_rate( + self.configuration.filter_format) + self.data_sources = self.draft.retrieve_data_sources() + self.tier_sources = self.draft.retrieve_tier_source() + self.set_metrics = self.draft.retrieve_set_metrics(False) + + tkinter.Grid.columnconfigure(self.root, 0, weight=1) + tkinter.Grid.columnconfigure(self.root, 1, weight=1) + # Menu Bar + self.menubar = tkinter.Menu(self.root) + self.filemenu = tkinter.Menu(self.menubar, tearoff=0) + self.filemenu.add_command(label="Open", command=self._open_draft_log) + self.datamenu = tkinter.Menu(self.menubar, tearoff=0) + self.datamenu.add_command( + label="View Sets", command=self._open_set_view_window) + + self.cardmenu = tkinter.Menu(self.menubar, tearoff=0) + self.cardmenu.add_command( + label="Taken Cards", command=self._open_taken_cards_window) + self.cardmenu.add_command( + label="Suggest Decks", command=self._open_suggest_deck_window) + self.cardmenu.add_command( + label="Compare Cards", command=self._open_card_compare_window) + + self.settingsmenu = tkinter.Menu(self.menubar, tearoff=0) + self.settingsmenu.add_command( + label="Settings", command=self._open_settings_window) + self.menubar.add_cascade(label="File", menu=self.filemenu) self.menubar.add_cascade(label="Data", menu=self.datamenu) self.menubar.add_cascade(label="Cards", menu=self.cardmenu) self.menubar.add_cascade(label="Settings", menu=self.settingsmenu) self.root.config(menu=self.menubar) - - style = Style() - style.map("Treeview", - foreground=FixedMap(style, "foreground"), - background=FixedMap(style, "background")) - - current_draft_label_frame = Frame(self.root) - self.current_draft_label = Label(current_draft_label_frame, text="Current Draft:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="e") - - current_draft_value_frame = Frame(self.root) - self.current_draft_value_label = Label(current_draft_value_frame, text="", font=f'{constants.FONT_SANS_SERIF} 9', anchor="w") - - data_source_label_frame = Frame(self.root) - self.data_source_label = Label(data_source_label_frame, text="Data Source:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="e") - - deck_colors_label_frame = Frame(self.root) - self.deck_colors_label = Label(deck_colors_label_frame, text="Deck Filter:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="e") - - self.data_source_selection = StringVar(self.root) + + current_draft_label_frame = tkinter.Frame(self.root) + self.current_draft_label = Label( + current_draft_label_frame, text="Current Draft:", style="MainSections.TLabel", anchor="e") + current_draft_value_frame = tkinter.Frame(self.root) + self.current_draft_value_label = Label( + current_draft_value_frame, text="", style="CurrentDraft.TLabel", anchor="w") + + data_source_label_frame = tkinter.Frame(self.root) + self.data_source_label = Label( + data_source_label_frame, text="Data Source:", style="MainSections.TLabel", anchor="e") + + deck_colors_label_frame = tkinter.Frame(self.root) + self.deck_colors_label = Label( + deck_colors_label_frame, text="Deck Filter:", style="MainSections.TLabel", anchor="e") + + self.data_source_selection = tkinter.StringVar(self.root) self.data_source_list = self.data_sources - - self.deck_stats_checkbox_value = IntVar(self.root) - self.missing_cards_checkbox_value = IntVar(self.root) - self.auto_highest_checkbox_value = IntVar(self.root) - self.curve_bonus_checkbox_value = IntVar(self.root) - self.color_bonus_checkbox_value = IntVar(self.root) - self.bayesian_average_checkbox_value = IntVar(self.root) - self.draft_log_checkbox_value = IntVar(self.root) - self.taken_alsa_checkbox_value = IntVar(self.root) - self.taken_ata_checkbox_value = IntVar(self.root) - self.taken_gpwr_checkbox_value = IntVar(self.root) - self.taken_ohwr_checkbox_value = IntVar(self.root) - self.taken_gndwr_checkbox_value = IntVar(self.root) - self.taken_iwd_checkbox_value = IntVar(self.root) - self.card_colors_checkbox_value = IntVar(self.root) - self.column_2_selection = StringVar(self.root) + + self.deck_stats_checkbox_value = tkinter.IntVar(self.root) + self.missing_cards_checkbox_value = tkinter.IntVar(self.root) + self.auto_highest_checkbox_value = tkinter.IntVar(self.root) + self.curve_bonus_checkbox_value = tkinter.IntVar(self.root) + self.color_bonus_checkbox_value = tkinter.IntVar(self.root) + self.bayesian_average_checkbox_value = tkinter.IntVar(self.root) + self.draft_log_checkbox_value = tkinter.IntVar(self.root) + self.taken_alsa_checkbox_value = tkinter.IntVar(self.root) + self.taken_ata_checkbox_value = tkinter.IntVar(self.root) + self.taken_gpwr_checkbox_value = tkinter.IntVar(self.root) + self.taken_ohwr_checkbox_value = tkinter.IntVar(self.root) + self.taken_gndwr_checkbox_value = tkinter.IntVar(self.root) + self.taken_iwd_checkbox_value = tkinter.IntVar(self.root) + self.taken_gdwr_checkbox_value = tkinter.IntVar(self.root) + self.card_colors_checkbox_value = tkinter.IntVar(self.root) + self.color_identity_checkbox_value = tkinter.IntVar(self.root) + self.taken_type_creature_checkbox_value = tkinter.IntVar(self.root) + self.taken_type_creature_checkbox_value.set(True) + self.taken_type_land_checkbox_value = tkinter.IntVar(self.root) + self.taken_type_land_checkbox_value.set(True) + self.taken_type_instant_sorcery_checkbox_value = tkinter.IntVar( + self.root) + self.taken_type_instant_sorcery_checkbox_value.set(True) + self.taken_type_other_checkbox_value = tkinter.IntVar(self.root) + self.taken_type_other_checkbox_value.set(True) + + self.column_2_selection = tkinter.StringVar(self.root) self.column_2_list = self.main_options_dict.keys() - self.column_3_selection = StringVar(self.root) + self.column_3_selection = tkinter.StringVar(self.root) self.column_3_list = self.main_options_dict.keys() - self.column_4_selection = StringVar(self.root) + self.column_4_selection = tkinter.StringVar(self.root) self.column_4_list = self.main_options_dict.keys() - self.column_5_selection = StringVar(self.root) + self.column_5_selection = tkinter.StringVar(self.root) self.column_5_list = self.extra_options_dict.keys() - self.column_6_selection = StringVar(self.root) + self.column_6_selection = tkinter.StringVar(self.root) self.column_6_list = self.extra_options_dict.keys() - self.column_7_selection = StringVar(self.root) + self.column_7_selection = tkinter.StringVar(self.root) self.column_7_list = self.extra_options_dict.keys() - self.filter_format_selection = StringVar(self.root) + self.filter_format_selection = tkinter.StringVar(self.root) self.filter_format_list = constants.DECK_FILTER_FORMAT_LIST - self.result_format_selection = StringVar(self.root) + self.result_format_selection = tkinter.StringVar(self.root) self.result_format_list = constants.RESULT_FORMAT_LIST - self.deck_filter_selection = StringVar(self.root) + self.deck_filter_selection = tkinter.StringVar(self.root) self.deck_filter_list = self.deck_colors.keys() - self.taken_filter_selection = StringVar(self.root) - self.taken_type_selection = StringVar(self.root) - - optionsStyle = Style() - optionsStyle.configure('my.TMenubutton', font=(constants.FONT_SANS_SERIF, 9)) - - data_source_option_frame = Frame(self.root) - self.data_source_options = OptionMenu(data_source_option_frame, self.data_source_selection, self.data_source_selection.get(), *self.data_source_list, style="my.TMenubutton") - + self.taken_filter_selection = tkinter.StringVar(self.root) + self.taken_type_selection = tkinter.StringVar(self.root) + + data_source_option_frame = tkinter.Frame(self.root) + self.data_source_options = OptionMenu(data_source_option_frame, self.data_source_selection, + self.data_source_selection.get(), *self.data_source_list, style="All.TMenubutton") + menu = self.root.nametowidget(self.data_source_options['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) + self.column_2_options = None self.column_3_options = None self.column_4_options = None @@ -316,409 +382,618 @@ def __init__(self, version, args): self.column_6_options = None self.column_7_options = None self.taken_table = None - - deck_colors_option_frame = Frame(self.root) - self.deck_colors_options = OptionMenu(deck_colors_option_frame, self.deck_filter_selection, self.deck_filter_selection.get(), *self.deck_filter_list, style="my.TMenubutton") - - self.refresh_button_frame = Frame(self.root) - self.refresh_button = Button(self.refresh_button_frame, command= lambda : self.UpdateCallback(True), text="Refresh") - - self.status_frame = Frame(self.root) - self.pack_pick_label = Label(self.status_frame, text="Pack: 0, Pick: 0", font=f'{constants.FONT_SANS_SERIF} 9 bold') - - self.pack_table_frame = Frame(self.root, width=10) - - headers = {"Column1" : {"width" : .46, "anchor" : W}, - "Column2" : {"width" : .18, "anchor" : CENTER}, - "Column3" : {"width" : .18, "anchor" : CENTER}, - "Column4" : {"width" : .18, "anchor" : CENTER}, - "Column5" : {"width" : .18, "anchor" : CENTER}, - "Column6" : {"width" : .18, "anchor" : CENTER}, - "Column7" : {"width" : .18, "anchor" : CENTER}} - style = Style() - style.configure("TButton", foreground="black") - style.configure("TEntry", foreground="black") - style.configure("Treeview.Heading", font=(constants.FONT_SANS_SERIF, 7), foreground="black") - - self.pack_table = CreateHeader(self.pack_table_frame, 0, 7, headers, self.configuration.table_width, True, True, constants.TABLE_STYLE, False) - - self.missing_frame = Frame(self.root) - self.missing_cards_label = Label(self.missing_frame, text = "Missing Cards", font=f'{constants.FONT_SANS_SERIF} 9 bold') - - self.missing_table_frame = Frame(self.root, width=10) - - self.missing_table = CreateHeader(self.missing_table_frame, 0, 7, headers, self.configuration.table_width, True, True, constants.TABLE_STYLE, False) - - self.stat_frame = Frame(self.root) - - self.stat_table = CreateHeader(self.root, 0, 7, constants.STATS_HEADER_CONFIG, self.configuration.table_width, True, True, constants.TABLE_STYLE, False) - self.stat_label = Label(self.stat_frame, text = "Draft Stats:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="e", width = 15) - - self.stat_options_selection = StringVar(self.root) - self.stat_options_list = [constants.CARD_TYPE_SELECTION_CREATURES,constants.CARD_TYPE_SELECTION_NONCREATURES,constants.CARD_TYPE_SELECTION_ALL] - - self.stat_options = OptionMenu(self.stat_frame, self.stat_options_selection, self.stat_options_list[0], *self.stat_options_list, style="my.TMenubutton") - self.stat_options.config(width=11) - - citation_label = Label(self.root, text="Powered by 17Lands*", font=f'{constants.FONT_SANS_SERIF} 9 ', anchor="e", borderwidth=2, relief="groove") - hotkey_label = Label(self.root, text="CTRL+G to Minimize", font=f'{constants.FONT_SANS_SERIF} 8 ', anchor="e" ) - footnote_label = Label(self.root, text="*This application is not endorsed by 17Lands", font=f'{constants.FONT_SANS_SERIF} 8 ', anchor="e") - - citation_label.grid(row = 0, column = 0, columnspan = 2) - current_draft_label_frame.grid(row = 1, column = 0, columnspan = 1, sticky = 'nsew') - current_draft_value_frame.grid(row = 1, column = 1, columnspan = 1, sticky = 'nsew') - data_source_label_frame.grid(row = 2, column = 0, columnspan = 1, sticky = 'nsew') - data_source_option_frame.grid(row = 2, column = 1, columnspan = 1, sticky = 'nsew') - deck_colors_label_frame.grid(row = 3, column = 0, columnspan = 1, sticky = 'nsew') - deck_colors_option_frame.grid(row = 3, column = 1, columnspan = 1, sticky = 'nsw') - hotkey_label.grid(row = 4, column = 0, columnspan = 2) - self.refresh_button_frame.grid(row = 5, column = 0, columnspan = 2, sticky = 'nsew') - self.status_frame.grid(row = 6, column = 0, columnspan = 2, sticky = 'nsew') - self.pack_table_frame.grid(row = 7, column = 0, columnspan = 2) - footnote_label.grid(row = 12, column = 0, columnspan = 2) - self.EnableDeckStates(self.deck_stats_checkbox_value.get()) - self.EnableMissingCards(self.missing_cards_checkbox_value.get()) - - self.refresh_button.pack(expand = True, fill = "both") - - self.pack_pick_label.pack(expand = False, fill = None) - self.pack_table.pack(expand = True, fill = 'both') - self.missing_cards_label.pack(expand = False, fill = None) - self.missing_table.pack(expand = True, fill = 'both') - self.stat_label.pack(side=LEFT, expand = True, fill = None) - self.stat_options.pack(side=RIGHT, expand = True, fill = None) - self.current_draft_label.pack(expand = True, fill = None, anchor="e") - self.current_draft_value_label.pack(expand = True, fill = None, anchor="w") - self.data_source_label.pack(expand = True, fill = None, anchor="e") - self.data_source_options.pack(expand = True, fill = None, anchor="w") - self.deck_colors_label.pack(expand = False, fill = None, anchor="e") - self.deck_colors_options.pack(expand = False, fill = None, anchor="w") - self.check_timestamp = 0 + + deck_colors_option_frame = tkinter.Frame(self.root) + self.deck_colors_options = OptionMenu(deck_colors_option_frame, self.deck_filter_selection, + self.deck_filter_selection.get(), *self.deck_filter_list, style="All.TMenubutton") + menu = self.root.nametowidget(self.deck_colors_options['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) + + self.refresh_button_frame = tkinter.Frame(self.root) + self.refresh_button = Button( + self.refresh_button_frame, command=lambda: self._update_overlay_callback(True), text="Refresh") + + self.status_frame = tkinter.Frame(self.root) + self.pack_pick_label = Label( + self.status_frame, text="Pack: 0, Pick: 0", style="MainSections.TLabel") + + self.pack_table_frame = tkinter.Frame(self.root, width=10) + + headers = {"Column1": {"width": .46, "anchor": tkinter.W}, + "Column2": {"width": .18, "anchor": tkinter.CENTER}, + "Column3": {"width": .18, "anchor": tkinter.CENTER}, + "Column4": {"width": .18, "anchor": tkinter.CENTER}, + "Column5": {"width": .18, "anchor": tkinter.CENTER}, + "Column6": {"width": .18, "anchor": tkinter.CENTER}, + "Column7": {"width": .18, "anchor": tkinter.CENTER}} + + self.pack_table = create_header(self.pack_table_frame, 0, self.fonts_dict["All.TableRow"], headers, + self.configuration.table_width, True, True, constants.TABLE_STYLE, False) + + self.missing_frame = tkinter.Frame(self.root) + self.missing_cards_label = Label( + self.missing_frame, text="Missing Cards", style="MainSections.TLabel") + + self.missing_table_frame = tkinter.Frame(self.root, width=10) + + self.missing_table = create_header(self.missing_table_frame, 0, self.fonts_dict["All.TableRow"], headers, + self.configuration.table_width, True, True, constants.TABLE_STYLE, False) + + self.stat_frame = tkinter.Frame(self.root) + + self.stat_table = create_header(self.root, 0, self.fonts_dict["All.TableRow"], constants.STATS_HEADER_CONFIG, + self.configuration.table_width, True, True, constants.TABLE_STYLE, False) + self.stat_label = Label(self.stat_frame, text="Draft Stats:", + style="MainSections.TLabel", anchor="e", width=15) + + self.stat_options_selection = tkinter.StringVar(self.root) + self.stat_options_list = [constants.CARD_TYPE_SELECTION_CREATURES, + constants.CARD_TYPE_SELECTION_NONCREATURES, + constants.CARD_TYPE_SELECTION_ALL] + + self.stat_options = OptionMenu(self.stat_frame, self.stat_options_selection, + self.stat_options_list[0], *self.stat_options_list, style="All.TMenubutton") + # self.stat_options.config(width=11) + menu = self.root.nametowidget(self.stat_options['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) + + citation_label = Label(self.root, text="Powered by 17Lands*", + anchor="e", borderwidth=2, relief="groove") + + hotkey_label = Label(self.root, text="CTRL+G to Minimize", + style="Notes.TLabel", anchor="e") + footnote_label = Label(self.root, text="*This application is not endorsed by 17Lands", + style="Notes.TLabel", anchor="e") + + citation_label.grid(row=0, column=0, columnspan=2) + current_draft_label_frame.grid( + row=1, column=0, columnspan=1, sticky='nsew') + current_draft_value_frame.grid( + row=1, column=1, columnspan=1, sticky='nsew') + data_source_label_frame.grid( + row=2, column=0, columnspan=1, sticky='nsew') + data_source_option_frame.grid( + row=2, column=1, columnspan=1, sticky='nsew') + deck_colors_label_frame.grid( + row=3, column=0, columnspan=1, sticky='nsew') + deck_colors_option_frame.grid( + row=3, column=1, columnspan=1, sticky='nsw') + hotkey_label.grid(row=4, column=0, columnspan=2) + self.refresh_button_frame.grid( + row=5, column=0, columnspan=2, sticky='nsew') + self.status_frame.grid(row=6, column=0, columnspan=2, sticky='nsew') + self.pack_table_frame.grid(row=7, column=0, columnspan=2) + footnote_label.grid(row=12, column=0, columnspan=2) + self._enable_deck_stats_table(self.deck_stats_checkbox_value.get()) + self._enable_missing_cards_table( + self.missing_cards_checkbox_value.get()) + + self.refresh_button.pack(expand=True, fill="both") + + self.pack_pick_label.pack(expand=False, fill=None) + self.pack_table.pack(expand=True, fill='both') + self.missing_cards_label.pack(expand=False, fill=None) + self.missing_table.pack(expand=True, fill='both') + self.stat_label.pack(side=tkinter.LEFT, expand=True, fill=None) + self.stat_options.pack(side=tkinter.RIGHT, expand=True, fill=None) + self.current_draft_label.pack(expand=True, fill=None, anchor="e") + self.current_draft_value_label.pack(expand=True, fill=None, anchor="w") + self.data_source_label.pack(expand=True, fill=None, anchor="e") + self.data_source_options.pack(expand=True, fill=None, anchor="w") + self.deck_colors_label.pack(expand=False, fill=None, anchor="e") + self.deck_colors_options.pack(expand=False, fill=None, anchor="w") + self.current_timestamp = 0 self.previous_timestamp = 0 - - self.UpdateSettingsData() + + self._update_settings_data() self.root.attributes("-topmost", True) - self.InitializeUI() - self.VersionCheck() - + self._initialize_overlay_widgets() + self._update_overlay_build() + if self.configuration.hotkey_enabled: - self.HotkeyListener() - - def HotkeyListener(self): - self.listener = Listener(on_press=lambda event: self.HotkeyPress(event)).start() - - def HotkeyPress(self, key): + self._start_hotkey_listener() + + def _set_os_configuration(self): + '''Configure the overlay based on the operating system''' + platform = sys.platform + + overlay_logger.info("Platform: %s", platform) + + if platform == constants.PLATFORM_ID_OSX: + self.configuration.hotkey_enabled = False + else: + self.root.tk.call("source", "dark_mode.tcl") + self._adjust_overlay_scale() + self._configure_fonts(platform) + + def _adjust_overlay_scale(self): + '''Adjust widget and font scale based on the scale_factor value in config.json''' + self.scale_factor = 1 + try: + if self.configuration.scale_factor > 0.0: + self.scale_factor = self.configuration.scale_factor + + overlay_logger.info("Scale Factor %.2f", + self.scale_factor) + except Exception as error: + overlay_logger.info("_adjust_overlay_scale Error: %s", error) + + return + + def _configure_fonts(self, platform): + '''Set size and family for the overlay fonts + - Negative font values are in pixels and positive font values are + in points (1/72 inch = 1 point) + ''' + try: + default_font = tkinter.font.nametofont("TkDefaultFont") + default_font.configure(size=self._scale_value(-12), + family=constants.FONT_SANS_SERIF) + + text_font = tkinter.font.nametofont("TkTextFont") + text_font.configure(size=self._scale_value(-12), + family=constants.FONT_SANS_SERIF) + + fixed_font = tkinter.font.nametofont("TkFixedFont") + fixed_font.configure(size=self._scale_value(-12), + family=constants.FONT_SANS_SERIF) + + menu_font = tkinter.font.nametofont("TkMenuFont") + menu_font.configure(size=self._scale_value(-12), + family=constants.FONT_SANS_SERIF) + + style = Style() + + style.configure("MainSections.TLabel", font=(constants.FONT_SANS_SERIF, + self._scale_value(-12), + "bold")) + + style.configure("CurrentDraft.TLabel", font=(constants.FONT_SANS_SERIF, + self._scale_value(-12))) + + style.configure("Notes.TLabel", font=(constants.FONT_SANS_SERIF, + self._scale_value(-11))) + + # style.configure("MainValues.TLabel", font=(constants.FONT_SANS_SERIF, + # self._scale_value(9))) + + style.configure("TooltipHeader.TLabel", font=(constants.FONT_SANS_SERIF, + self._scale_value(-17), + "bold")) + + style.configure("Status.TLabel", font=(constants.FONT_SANS_SERIF, + self._scale_value(-15), + "bold")) + + style.configure("SetOptions.TLabel", font=(constants.FONT_SANS_SERIF, + self._scale_value(-13), + "bold")) + + style.configure("All.TMenubutton", font=(constants.FONT_SANS_SERIF, + self._scale_value(-12))) + self.fonts_dict["All.TMenubutton"] = (constants.FONT_SANS_SERIF, + self._scale_value(-12)) + + self.fonts_dict["All.TableRow"] = self._scale_value(-11) + self.fonts_dict["Sets.TableRow"] = self._scale_value(-13) + + style.configure("Taken.TCheckbutton", font=(constants.FONT_SANS_SERIF, + self._scale_value(-11))) + + style.map("Treeview", + foreground=fixed_map(style, "foreground"), + background=fixed_map(style, "background")) + + style.configure("Treeview", rowheight=self._scale_value(25)) + + style.configure("Taken.Treeview", rowheight=self._scale_value(25)) + + style.configure("Suggest.Treeview", + rowheight=self._scale_value(25)) + + style.configure("Set.Treeview", rowheight=self._scale_value(25)) + + style.configure("Treeview.Heading", font=(constants.FONT_SANS_SERIF, + self._scale_value(-9))) + + if platform == constants.PLATFORM_ID_WINDOWS: + style.configure("TButton", foreground="black") + style.configure("Treeview.Heading", foreground="black") + style.configure("TEntry", foreground="black") + + except Exception as error: + overlay_logger.info("_configure_fonts Error: %s", error) + + def _start_hotkey_listener(self): + '''Start listener that detects the minimize hotkey''' + self.listener = Listener( + on_press=lambda event: self._process_hotkey_press(event)).start() + + def _process_hotkey_press(self, key): + '''Determine if the minimize hotkey was pressed''' if key == KeyCode.from_char(constants.HOTKEY_CTRL_G): - self.WindowLift() - - def MainLoop(self): + self.lift_window() + + def main_loop(self): + '''Run the TKinter overlay''' self.root.mainloop() - - def DeckFilterColors(self, cards, selected_option): + + def _identify_auto_colors(self, cards, selected_option): + '''Update the Deck Filter option menu when the Auto option is selected''' filtered_colors = [constants.FILTER_OPTION_ALL_DECKS] try: #selected_option = self.deck_filter_selection.get() selected_color = self.deck_colors[selected_option] - filtered_colors = CL.OptionFilter(cards, selected_color, self.set_metrics, self.configuration) + filtered_colors = CL.option_filter( + cards, selected_color, self.set_metrics, self.configuration) if selected_color == constants.FILTER_OPTION_AUTO: new_key = f"{constants.FILTER_OPTION_AUTO} ({'/'.join(filtered_colors)})" if new_key != selected_option: self.deck_colors.pop(selected_option) - new_dict = {new_key : constants.FILTER_OPTION_AUTO} + new_dict = {new_key: constants.FILTER_OPTION_AUTO} new_dict.update(self.deck_colors) self.deck_colors = new_dict - self.UpdateColumnOptions() + self._update_column_options() except Exception as error: - overlay_logger.info(f"DeckFilterColors Error: {error}") - + overlay_logger.info("__identify_auto_colors Error: %s", error) + return filtered_colors - - def UpdatePackTable(self, card_list, taken_cards, filtered_colors, fields): + + def _update_pack_table(self, card_list, filtered_colors, fields): + '''Update the table that lists the cards within the current pack''' try: - filtered_list = CL.CardFilter(card_list, - taken_cards, - filtered_colors, - fields, - self.set_metrics, - self.tier_data, - self.configuration, - self.configuration.curve_bonus_enabled, - self.configuration.color_bonus_enabled) - + result_class = CL.CardResult( + self.set_metrics, self.tier_data, self.configuration, self.draft.current_pick) + result_list = result_class.return_results( + card_list, filtered_colors, fields) + # clear the previous rows for row in self.pack_table.get_children(): self.pack_table.delete(row) - - list_length = len(filtered_list) - + + list_length = len(result_list) + if list_length: - self.pack_table.config(height = list_length) + self.pack_table.config(height=list_length) else: self.pack_table.config(height=0) - - #Update the filtered column header with the filtered colors - last_field_index = TableColumnControl(self.pack_table, fields) - - filtered_list = sorted(filtered_list, key=lambda d: CL.FieldProcessSort(d["results"][last_field_index]), reverse=True) - - for count, card in enumerate(filtered_list): - row_tag = TableRowTag(self.configuration.card_colors_enabled, card[constants.DATA_FIELD_COLORS], count) + + # Update the filtered column header with the filtered colors + last_field_index = control_table_column(self.pack_table, fields) + + result_list = sorted(result_list, key=lambda d: CL.field_process_sort( + d["results"][last_field_index]), reverse=True) + + for count, card in enumerate(result_list): + row_tag = identify_table_row_tag( + self.configuration.card_colors_enabled, + card[constants.DATA_FIELD_MANA_COST], + count) field_values = tuple(card["results"]) - self.pack_table.insert("",index = count, iid = count, values = field_values, tag = (row_tag,)) - self.pack_table.bind("<>", lambda event: self.OnClickTable(event, table=self.pack_table, card_list=card_list, selected_color=filtered_colors)) + self.pack_table.insert( + "", index=count, iid=count, values=field_values, tag=(row_tag,)) + self.pack_table.bind("<>", lambda event: self._process_table_click( + event, table=self.pack_table, card_list=card_list, selected_color=filtered_colors)) except Exception as error: - overlay_logger.info(f"UpdatePackTable Error: {error}") - - def UpdateMissingTable(self, current_pack, previous_pack, picked_cards, taken_cards, filtered_colors, fields): + overlay_logger.info("__update_pack_table Error: %s", error) + + def _update_missing_table(self, current_pack, previous_pack, picked_cards, filtered_colors, fields): + '''Update the table that lists the cards that are missing from the current pack''' try: for row in self.missing_table.get_children(): self.missing_table.delete(row) - - #Update the filtered column header with the filtered colors - last_field_index = TableColumnControl(self.missing_table, fields) - if len(previous_pack) == 0: + + # Update the filtered column header with the filtered colors + last_field_index = control_table_column(self.missing_table, fields) + if not previous_pack: self.missing_table.config(height=0) else: - missing_cards = [x for x in previous_pack if x not in current_pack] - + missing_cards = [ + x for x in previous_pack if x not in current_pack] + list_length = len(missing_cards) - + if list_length: - self.missing_table.config(height = list_length) + self.missing_table.config(height=list_length) else: - self.missing_table.config(height=0) - + self.missing_table.config(height=0) + if list_length: - filtered_list = CL.CardFilter(missing_cards, - taken_cards, - filtered_colors, - fields, - self.set_metrics, - self.tier_data, - self.configuration, - False, - False) - - filtered_list = sorted(filtered_list, key=lambda d: CL.FieldProcessSort(d["results"][last_field_index]), reverse=True) - - #filtered_list.sort(key = functools.cmp_to_key(CL.CompareRatings)) - for count, card in enumerate(filtered_list): - row_tag = TableRowTag(self.configuration.card_colors_enabled, card[constants.DATA_FIELD_COLORS], count) + result_class = CL.CardResult( + self.set_metrics, self.tier_data, self.configuration, self.draft.current_pick) + result_list = result_class.return_results( + missing_cards, filtered_colors, fields) + + result_list = sorted(result_list, key=lambda d: CL.field_process_sort( + d["results"][last_field_index]), reverse=True) + + for count, card in enumerate(result_list): + row_tag = identify_table_row_tag( + self.configuration.card_colors_enabled, + card[constants.DATA_FIELD_MANA_COST], + count) for index, field in enumerate(fields.values()): if field == constants.DATA_FIELD_NAME: - card["results"][index] = f'*{card["results"][index]}' if card["results"][index] in picked_cards else card["results"][index] + card["results"][index] = f'*{card["results"][index]}' if card[ + "results"][index] in picked_cards else card["results"][index] field_values = tuple(card["results"]) - self.missing_table.insert("",index = count, iid = count, values = field_values, tag = (row_tag,)) - self.missing_table.bind("<>", lambda event: self.OnClickTable(event, table=self.missing_table, card_list=missing_cards, selected_color=filtered_colors)) + self.missing_table.insert( + "", index=count, iid=count, values=field_values, tag=(row_tag,)) + self.missing_table.bind("<>", lambda event: self._process_table_click( + event, table=self.missing_table, card_list=missing_cards, selected_color=filtered_colors)) except Exception as error: - overlay_logger.info(f"UpdateMissingTable Error: {error}") + overlay_logger.info("__update_missing_table Error: %s", error) - def ClearCompareTable(self, compare_table, matching_cards): + def _clear_compare_table(self, compare_table, matching_cards): + '''Clear the rows within the Card Compare table''' matching_cards.clear() compare_table.delete(*compare_table.get_children()) compare_table.config(height=0) - def UpdateCompareTable(self, compare_table, matching_cards, entry_box, card_list, filtered_colors, fields): + def _update_compare_table(self, compare_table, matching_cards, entry_box, card_list, filtered_colors, fields): + '''Update the Card Compare table that lists the searched cards''' try: added_card = entry_box.get() - if len(added_card): - cards = [card_list[x] for x in card_list if card_list[x][constants.DATA_FIELD_NAME] == added_card and card_list[x] not in matching_cards] - entry_box.delete(0,END) - if len(cards): + if added_card: + cards = [card_list[x] for x in card_list if card_list[x] + [constants.DATA_FIELD_NAME] == added_card and card_list[x] not in matching_cards] + entry_box.delete(0, tkinter.END) + if cards: matching_cards.append(cards[0]) - filtered_list = CL.CardFilter(matching_cards, - matching_cards, - filtered_colors, - fields, - self.set_metrics, - self.tier_data, - self.configuration, - False, - False) - + result_class = CL.CardResult( + self.set_metrics, self.tier_data, self.configuration, self.draft.current_pick) + result_list = result_class.return_results( + matching_cards, filtered_colors, fields) + compare_table.delete(*compare_table.get_children()) - #Update the filtered column header with the filtered colors - last_field_index = TableColumnControl(compare_table, fields) + # Update the filtered column header with the filtered colors + last_field_index = control_table_column(compare_table, fields) + + result_list = sorted(result_list, key=lambda d: CL.field_process_sort( + d["results"][last_field_index]), reverse=True) + + list_length = len(result_list) - filtered_list = sorted(filtered_list, key=lambda d: CL.FieldProcessSort(d["results"][last_field_index]), reverse=True) - - list_length = len(filtered_list) - if list_length: - compare_table.config(height = list_length) + compare_table.config(height=list_length) else: compare_table.config(height=0) - - for count, card in enumerate(filtered_list): - row_tag = TableRowTag(self.configuration.card_colors_enabled, card[constants.DATA_FIELD_COLORS], count) + + for count, card in enumerate(result_list): + row_tag = identify_table_row_tag( + self.configuration.card_colors_enabled, + card[constants.DATA_FIELD_MANA_COST], + count) field_values = tuple(card["results"]) - compare_table.insert("",index = count, iid = count, values = field_values, tag = (row_tag,)) - compare_table.bind("<>", lambda event: self.OnClickTable(event, table=compare_table, card_list=matching_cards, selected_color=filtered_colors)) + compare_table.insert( + "", index=count, iid=count, values=field_values, tag=(row_tag,)) + compare_table.bind("<>", lambda event: self._process_table_click( + event, table=compare_table, card_list=matching_cards, selected_color=filtered_colors)) except Exception as error: - overlay_logger.info(f"UpdateCompareTable Error: {error}") + overlay_logger.info("__update_compare_table Error: %s", error) - def UpdateTakenTable(self, *args): + def _update_taken_table(self, *args): + '''Update the table that lists the taken cards''' try: - while(True): - if self.taken_table == None: + while True: + if self.taken_table is None: break - fields = {"Column1" : constants.DATA_FIELD_NAME, - "Column2" : constants.DATA_FIELD_COUNT, - "Column3" : constants.DATA_FIELD_COLORS, - "Column4" : (constants.DATA_FIELD_ALSA if self.taken_alsa_checkbox_value.get() else constants.DATA_FIELD_DISABLED), - "Column5" : (constants.DATA_FIELD_ATA if self.taken_ata_checkbox_value.get() else constants.DATA_FIELD_DISABLED), - "Column6" : (constants.DATA_FIELD_IWD if self.taken_iwd_checkbox_value.get() else constants.DATA_FIELD_DISABLED), - "Column7" : (constants.DATA_FIELD_GPWR if self.taken_gpwr_checkbox_value.get() else constants.DATA_FIELD_DISABLED), - "Column8" : (constants.DATA_FIELD_OHWR if self.taken_ohwr_checkbox_value.get() else constants.DATA_FIELD_DISABLED), - "Column9" : (constants.DATA_FIELD_GNDWR if self.taken_gndwr_checkbox_value.get() else constants.DATA_FIELD_DISABLED), - "Column10" : constants.DATA_FIELD_GIHWR} - - - taken_cards = self.draft.TakenCards() - - filtered_colors = self.DeckFilterColors(taken_cards, self.taken_filter_selection.get()) - - #Filter the card types - #filtered_cards = CL.DeckColorSearch(taken_cards, constants.CARD_COLORS_DICT.values(), constants.CARD_TYPE_DICT[self.taken_type_selection.get()], True, True, True) - - stacked_cards = CL.StackCards(taken_cards) + fields = {"Column1": constants.DATA_FIELD_NAME, + "Column2": constants.DATA_FIELD_COUNT, + "Column3": constants.DATA_FIELD_COLORS, + "Column4": (constants.DATA_FIELD_ALSA if self.taken_alsa_checkbox_value.get() else constants.DATA_FIELD_DISABLED), + "Column5": (constants.DATA_FIELD_ATA if self.taken_ata_checkbox_value.get() else constants.DATA_FIELD_DISABLED), + "Column6": (constants.DATA_FIELD_IWD if self.taken_iwd_checkbox_value.get() else constants.DATA_FIELD_DISABLED), + "Column7": (constants.DATA_FIELD_GPWR if self.taken_gpwr_checkbox_value.get() else constants.DATA_FIELD_DISABLED), + "Column8": (constants.DATA_FIELD_OHWR if self.taken_ohwr_checkbox_value.get() else constants.DATA_FIELD_DISABLED), + "Column9": (constants.DATA_FIELD_GDWR if self.taken_gdwr_checkbox_value.get() else constants.DATA_FIELD_DISABLED), + "Column10": (constants.DATA_FIELD_GNDWR if self.taken_gndwr_checkbox_value.get() else constants.DATA_FIELD_DISABLED), + "Column11": constants.DATA_FIELD_GIHWR} + + taken_cards = self.draft.retrieve_taken_cards() + + filtered_colors = self._identify_auto_colors( + taken_cards, self.taken_filter_selection.get()) + + # Apply the card type filters + if not (self.taken_type_creature_checkbox_value.get() and + self.taken_type_land_checkbox_value.get() and + self.taken_type_instant_sorcery_checkbox_value.get() and + self.taken_type_other_checkbox_value.get()): + card_types = [] + + if self.taken_type_creature_checkbox_value.get(): + card_types.append(constants.CARD_TYPE_CREATURE) + + if self.taken_type_land_checkbox_value.get(): + card_types.append(constants.CARD_TYPE_LAND) + + if self.taken_type_instant_sorcery_checkbox_value.get(): + card_types.extend( + [constants.CARD_TYPE_INSTANT, constants.CARD_TYPE_SORCERY]) + + if self.taken_type_other_checkbox_value.get(): + card_types.extend([constants.CARD_TYPE_ARTIFACT, + constants.CARD_TYPE_ENCHANTMENT, + constants.CARD_TYPE_PLANESWALKER]) + + taken_cards = CL.deck_card_search(taken_cards, + constants.CARD_COLORS, + card_types, + True, + True, + True) + + stacked_cards = CL.stack_cards(taken_cards) for row in self.taken_table.get_children(): self.taken_table.delete(row) - filtered_list = CL.CardFilter(stacked_cards, - taken_cards, - filtered_colors, - fields, - self.set_metrics, - self.tier_data, - self.configuration, - False, - False) + result_class = CL.CardResult( + self.set_metrics, self.tier_data, self.configuration, self.draft.current_pick) + result_list = result_class.return_results( + stacked_cards, filtered_colors, fields) - last_field_index = TableColumnControl(self.taken_table, fields) + last_field_index = control_table_column( + self.taken_table, fields) - filtered_list = sorted(filtered_list, key=lambda d: CL.FieldProcessSort(d["results"][last_field_index]), reverse=True) + result_list = sorted(result_list, key=lambda d: CL.field_process_sort( + d["results"][last_field_index]), reverse=True) - if len(filtered_list): - self.taken_table.config(height = min(len(filtered_list), 20)) + if len(result_list): + self.taken_table.config(height=min(len(result_list), 20)) else: self.taken_table.config(height=1) - for count, card in enumerate(filtered_list): + for count, card in enumerate(result_list): field_values = tuple(card["results"]) - row_tag = TableRowTag(self.configuration.card_colors_enabled, card[constants.DATA_FIELD_COLORS], count) - self.taken_table.insert("",index = count, iid = count, values = field_values, tag = (row_tag,)) - self.taken_table.bind("<>", lambda event: self.OnClickTable(event, table=self.taken_table, card_list=filtered_list, selected_color=filtered_colors)) + row_tag = identify_table_row_tag( + self.configuration.card_colors_enabled, + card[constants.DATA_FIELD_MANA_COST], + count) + self.taken_table.insert( + "", index=count, iid=count, values=field_values, tag=(row_tag,)) + self.taken_table.bind("<>", lambda event: self._process_table_click( + event, table=self.taken_table, card_list=result_list, selected_color=filtered_colors)) break except Exception as error: - overlay_logger.info(f"UpdateTakenTable Error: {error}") - - def UpdateSuggestDeckTable(self, suggest_table, selected_color, suggested_decks, color_options): - try: + overlay_logger.info("__update_taken_table Error: %s", error) + + def _update_suggest_table(self, suggest_table, selected_color, suggested_decks, color_options): + '''Update the table that lists the suggested decks''' + try: color = color_options[selected_color.get()] suggested_deck = suggested_decks[color]["deck_cards"] - suggested_deck.sort(key=lambda x : x[constants.DATA_FIELD_CMC], reverse = False) + suggested_deck.sort( + key=lambda x: x[constants.DATA_FIELD_CMC], reverse=False) for row in suggest_table.get_children(): suggest_table.delete(row) list_length = len(suggested_deck) if list_length: - suggest_table.config(height = list_length) + suggest_table.config(height=list_length) else: suggest_table.config(height=0) - + for count, card in enumerate(suggested_deck): - row_tag = TableRowTag(self.configuration.card_colors_enabled, card[constants.DATA_FIELD_COLORS], count) - suggest_table.insert("",index = count, values = (card[constants.DATA_FIELD_NAME], - "%d" % card[constants.DATA_FIELD_COUNT], - card[constants.DATA_FIELD_COLORS], - card[constants.DATA_FIELD_CMC], - card[constants.DATA_FIELD_TYPES]), tag = (row_tag,)) - suggest_table.bind("<>", lambda event: self.OnClickTable(event, table=suggest_table, card_list=suggested_deck, selected_color=[color])) - + row_tag = identify_table_row_tag( + self.configuration.card_colors_enabled, + card[constants.DATA_FIELD_MANA_COST], + count) + + if constants.CARD_TYPE_LAND in card[constants.DATA_FIELD_TYPES]: + card_colors = "".join(card[constants.DATA_FIELD_COLORS]) + else: + card_colors = "".join(list(CL.card_colors(card[constants.DATA_FIELD_MANA_COST]).keys()) + if not self.configuration.color_identity_enabled + else card[constants.DATA_FIELD_COLORS]) + + suggest_table.insert("", index=count, values=(card[constants.DATA_FIELD_NAME], + f"{card[constants.DATA_FIELD_COUNT]}", + card_colors, + card[constants.DATA_FIELD_CMC], + card[constants.DATA_FIELD_TYPES]), tag=(row_tag,)) + suggest_table.bind("<>", lambda event: self._process_table_click( + event, table=suggest_table, card_list=suggested_deck, selected_color=[color])) + except Exception as error: - overlay_logger.info(f"UpdateSuggestTable Error: {error}") - - def UpdateDeckStatsTable(self, taken_cards, filter_type, total_width): - try: - filter = constants.CARD_TYPE_DICT[filter_type] + overlay_logger.info("_update_suggest_table Error: %s", error) + + def _update_deck_stats_table(self, taken_cards, filter_type, total_width): + '''Update the table that lists the draft stats''' + try: + card_types = constants.CARD_TYPE_DICT[filter_type] - #colors = {constants.CARD_COLOR_LABEL_BLACK:constants.CARD_COLOR_SYMBOL_BLACK,constants.CARD_COLOR_LABEL_BLUE:constants.CARD_COLOR_SYMBOL_BLUE, constants.CARD_COLOR_LABEL_GREEN:constants.CARD_COLOR_SYMBOL_GREEN, constants.CARD_COLOR_LABEL_RED:constants.CARD_COLOR_SYMBOL_RED, constants.CARD_COLOR_LABEL_WHITE:constants.CARD_COLOR_SYMBOL_WHITE, constants.CARD_COLOR_SYMBOL_NONE:""} colors_filtered = {} - for color,symbol in constants.CARD_COLORS_DICT.items(): - if symbol == "": - card_colors_sorted = CL.DeckColorSearch(taken_cards, symbol, filter, True, True, False) + for color, symbol in constants.CARD_COLORS_DICT.items(): + if symbol: + card_colors_sorted = CL.deck_card_search( + taken_cards, symbol, card_types[0], card_types[1], card_types[2], card_types[3]) else: - card_colors_sorted = CL.DeckColorSearch(taken_cards, symbol, filter, True, False, True) - cmc_total, total, distribution = CL.ColorCmc(card_colors_sorted) + card_colors_sorted = CL.deck_card_search( + taken_cards, symbol, card_types[0], card_types[1], True, False) + card_metrics = CL.deck_metrics(card_colors_sorted) colors_filtered[color] = {} colors_filtered[color]["symbol"] = symbol - colors_filtered[color]["total"] = total - colors_filtered[color]["distribution"] = distribution - - #Sort list by total - colors_filtered = dict(sorted(colors_filtered.items(), key = lambda item: item[1]["total"], reverse = True)) - + colors_filtered[color]["total"] = card_metrics.total_cards + colors_filtered[color]["distribution"] = card_metrics.distribution_all + + # Sort list by total + colors_filtered = dict(sorted(colors_filtered.items( + ), key=lambda item: item[1]["total"], reverse=True)) + for row in self.stat_table.get_children(): self.stat_table.delete(row) - #Adjust the width for each column + if total_width == 1: + self.stat_table.config(height=0) + self._enable_deck_stats_table(False) + return + + # Adjust the width for each column + width = total_width - 5 for column in self.stat_table["columns"]: - self.stat_table.column(column, width = int(constants.STATS_HEADER_CONFIG[column]["width"] * total_width)) + column_width = min(int(math.ceil( + constants.STATS_HEADER_CONFIG[column]["width"] * total_width)), width) + width -= column_width + self.stat_table.column(column, width=column_width) list_length = len(colors_filtered) if list_length: - self.stat_table.config(height = list_length) + self.stat_table.config(height=list_length) else: self.stat_table.config(height=0) - - count = 0 + # return + for count, (color, values) in enumerate(colors_filtered.items()): - #row_tag = CL.RowColorTag(values["symbol"]) - row_tag = TableRowTag(self.configuration.card_colors_enabled, values["symbol"], count) - self.stat_table.insert("",index = count, values = (color, - values["distribution"][1], - values["distribution"][2], - values["distribution"][3], - values["distribution"][4], - values["distribution"][5], - values["distribution"][6], - values["total"]), tag = (row_tag,)) - count += 1 + row_tag = identify_table_row_tag( + self.configuration.card_colors_enabled, values["symbol"], count) + self.stat_table.insert("", index=count, values=(color, + values["distribution"][1], + values["distribution"][2], + values["distribution"][3], + values["distribution"][4], + values["distribution"][5], + values["distribution"][6], + values["total"]), tag=(row_tag,)) except Exception as error: - overlay_logger.info(f"UpdateDeckStats Error: {error}") - - def UpdatePackPick(self, pack, pick): + overlay_logger.info("__update_deck_stats_table Error: %s", error) + + def _update_pack_pick_label(self, pack, pick): + '''Update the label that lists the pack and pick numbers''' try: - new_label = "Pack: %u, Pick: %u" % (pack, pick) - self.pack_pick_label.config(text = new_label) - + new_label = f"Pack: {pack}, Pick: {pick}" + self.pack_pick_label.config(text=new_label) + except Exception as error: - overlay_logger.info(f"UpdatePackPick Error: {error}") - - def UpdateCurrentDraft(self, set, draft_type): - try: + overlay_logger.info("__update_pack_pick_label Error: %s", error) + + def _update_current_draft_label(self, card_set, draft_type): + '''Update the label that lists the current draft set and type (e.g., DMU PremierDraft)''' + try: draft_type_string = '' - + for key, value in constants.LIMITED_TYPES_DICT.items(): - if constants.LIMITED_TYPES_DICT[key] == draft_type: + if value == draft_type: draft_type_string = key - - new_label = f" {set[0]} {draft_type_string}" if set else " None" - self.current_draft_value_label.config(text = new_label) + break + + new_label = f" {card_set[0]} {draft_type_string}" if card_set else " None" + self.current_draft_value_label.config(text=new_label) except Exception as error: - overlay_logger.info(f"UpdateCurrentDraft Error: {error}") - - def UpdateSourceOptions(self, new_list): - self.ControlTrace(False) + overlay_logger.info( + "__update_current_draft_label Error: %s", error) + + def _update_data_source_options(self, new_list): + '''Update the option menu that lists the available data sets for the current draft set (i.e., QuickDraft, PremierDraft, TradDraft, etc.)''' + self._control_trace(False) try: if new_list: self.data_source_selection.set(next(iter(self.data_sources))) @@ -727,49 +1002,56 @@ def UpdateSourceOptions(self, new_list): self.data_source_list = [] - for key, data in self.data_sources.items(): - menu.add_command(label=key, - command=lambda value=key: self.data_source_selection.set(value)) + for key in self.data_sources: + menu.add_command(label=key, + command=lambda value=key: self.data_source_selection.set(value)) self.data_source_list.append(key) - elif self.data_source_selection.get() not in self.data_sources.keys(): + elif self.data_source_selection.get() not in self.data_sources: self.data_source_selection.set(next(iter(self.data_sources))) + except Exception as error: + overlay_logger.info( + "__update_data_source_options Error: %s", error) + self._control_trace(True) - except Exception as error: - overlay_logger.info(f"UpdateSourceOptions Error: {error}") - - self.ControlTrace(True) - - def UpdateColumnOptions(self): - self.ControlTrace(False) - try: + def _update_column_options(self): + '''Update the option menus whenever the application settings change''' + self._control_trace(False) + try: if self.filter_format_selection.get() not in self.filter_format_list: - self.filter_format_selection.set(constants.DECK_FILTER_FORMAT_COLORS) + self.filter_format_selection.set( + constants.DECK_FILTER_FORMAT_COLORS) if self.result_format_selection.get() not in self.result_format_list: - self.result_format_selection.set(constants.RESULT_FORMAT_WIN_RATE) - if self.column_2_selection.get() not in self.main_options_dict.keys(): + self.result_format_selection.set( + constants.RESULT_FORMAT_WIN_RATE) + if self.column_2_selection.get() not in self.main_options_dict: self.column_2_selection.set(constants.COLUMN_2_DEFAULT) - if self.column_3_selection.get() not in self.main_options_dict.keys(): + if self.column_3_selection.get() not in self.main_options_dict: self.column_3_selection.set(constants.COLUMN_3_DEFAULT) - if self.column_4_selection.get() not in self.main_options_dict.keys(): + if self.column_4_selection.get() not in self.main_options_dict: self.column_4_selection.set(constants.COLUMN_4_DEFAULT) - if self.column_5_selection.get() not in self.extra_options_dict.keys(): + if self.column_5_selection.get() not in self.extra_options_dict: self.column_5_selection.set(constants.COLUMN_5_DEFAULT) - if self.column_6_selection.get() not in self.extra_options_dict.keys(): + if self.column_6_selection.get() not in self.extra_options_dict: self.column_6_selection.set(constants.COLUMN_6_DEFAULT) - if self.column_7_selection.get() not in self.extra_options_dict.keys(): + if self.column_7_selection.get() not in self.extra_options_dict: self.column_7_selection.set(constants.COLUMN_7_DEFAULT) - if self.deck_filter_selection.get() not in self.deck_colors.keys(): - selection = [k for k in self.deck_colors.keys() if constants.DECK_FILTER_DEFAULT in k] - self.deck_filter_selection.set(selection[0] if len(selection) else constants.DECK_FILTER_DEFAULT) - if self.taken_filter_selection.get() not in self.deck_colors.keys(): - selection = [k for k in self.deck_colors.keys() if constants.DECK_FILTER_DEFAULT in k] - self.taken_filter_selection.set(selection[0] if len(selection) else constants.DECK_FILTER_DEFAULT) - if self.taken_type_selection.get() not in constants.CARD_TYPE_DICT.keys(): - self.taken_type_selection.set(constants.CARD_TYPE_SELECTION_ALL) - + if self.deck_filter_selection.get() not in self.deck_colors: + selection = [k for k in self.deck_colors.keys( + ) if constants.DECK_FILTER_DEFAULT in k] + self.deck_filter_selection.set(selection[0] if len( + selection) else constants.DECK_FILTER_DEFAULT) + if self.taken_filter_selection.get() not in self.deck_colors: + selection = [k for k in self.deck_colors.keys( + ) if constants.DECK_FILTER_DEFAULT in k] + self.taken_filter_selection.set(selection[0] if len( + selection) else constants.DECK_FILTER_DEFAULT) + if self.taken_type_selection.get() not in constants.CARD_TYPE_DICT: + self.taken_type_selection.set( + constants.CARD_TYPE_SELECTION_ALL) + deck_colors_menu = self.deck_colors_options["menu"] deck_colors_menu.delete(0, "end") column_2_menu = None @@ -804,162 +1086,233 @@ def UpdateColumnOptions(self): self.column_7_list = [] self.deck_filter_list = [] - for key in self.main_options_dict.keys(): + for key in self.main_options_dict: if column_2_menu: - column_2_menu.add_command(label=key, - command=lambda value=key: self.column_2_selection.set(value)) + column_2_menu.add_command(label=key, + command=lambda value=key: self.column_2_selection.set(value)) if column_3_menu: - column_3_menu.add_command(label=key, - command=lambda value=key: self.column_3_selection.set(value)) + column_3_menu.add_command(label=key, + command=lambda value=key: self.column_3_selection.set(value)) if column_4_menu: - column_4_menu.add_command(label=key, - command=lambda value=key: self.column_4_selection.set(value)) + column_4_menu.add_command(label=key, + command=lambda value=key: self.column_4_selection.set(value)) - #self.deck_colors_options_list.append(data) + # self.deck_colors_options_list.append(data) self.column_2_list.append(key) self.column_3_list.append(key) self.column_4_list.append(key) - - for key in self.extra_options_dict.keys(): + + for key in self.extra_options_dict: if column_5_menu: - column_5_menu.add_command(label=key, - command=lambda value=key: self.column_5_selection.set(value)) + column_5_menu.add_command(label=key, + command=lambda value=key: self.column_5_selection.set(value)) if column_6_menu: - column_6_menu.add_command(label=key, - command=lambda value=key: self.column_6_selection.set(value)) + column_6_menu.add_command(label=key, + command=lambda value=key: self.column_6_selection.set(value)) if column_7_menu: - column_7_menu.add_command(label=key, - command=lambda value=key: self.column_7_selection.set(value)) - + column_7_menu.add_command(label=key, + command=lambda value=key: self.column_7_selection.set(value)) + self.column_5_list.append(key) self.column_6_list.append(key) self.column_7_list.append(key) - - for key in self.deck_colors.keys(): - deck_colors_menu.add_command(label=key, - command=lambda value=key: self.deck_filter_selection.set(value)) + + for key in self.deck_colors: + deck_colors_menu.add_command(label=key, + command=lambda value=key: self.deck_filter_selection.set(value)) self.deck_filter_list.append(key) - + except Exception as error: - overlay_logger.info(f"UpdateColumnOptions Error: {error}") - - self.ControlTrace(True) - - def DefaultSettingsCallback(self, *args): - CL.ResetConfig() - self.configuration = CL.ReadConfig() - self.UpdateSettingsData() - self.UpdateDraftData() - self.UpdateCallback(False) - - def UpdateSourceCallback(self, *args): - self.UpdateSettingsStorage() - self.UpdateDraftData() - self.UpdateSettingsData() - self.UpdateCallback(False) - - def UpdateSettingsCallback(self, *args): - self.UpdateSettingsStorage() - self.UpdateSettingsData() - self.UpdateCallback(False) - - def UpdateDraftData(self): - self.draft.RetrieveSetData(self.data_sources[self.data_source_selection.get()]) - self.set_metrics = self.draft.RetrieveSetMetrics(False) - self.deck_colors = self.draft.RetrieveColorWinRate(self.filter_format_selection.get()) - self.tier_data, tier_dict = self.draft.RetrieveTierData(self.tier_sources) + overlay_logger.info("__update_column_options Error: %s", error) + + self._control_trace(True) + + def _default_settings_callback(self, *args): + '''Callback function that's called when the Default Settings button is pressed''' + CL.reset_config() + self.configuration = CL.read_config() + self._update_settings_data() + self._update_draft_data() + self._update_overlay_callback(False) + + def _update_source_callback(self, *args): + '''Callback function that collects the set data a new data source is selected''' + self._update_settings_storage() + self._update_draft_data() + self._update_settings_data() + self._update_overlay_callback(False) + + def _update_settings_callback(self, *args): + '''Callback function reconfigures the application whenever the settings change''' + self._update_settings_storage() + self._update_settings_data() + self._update_overlay_callback(False) + + def _update_draft_data(self): + '''Function that collects pertinent draft data from the LogScanner class''' + self.draft.retrieve_set_data( + self.data_sources[self.data_source_selection.get()]) + self.set_metrics = self.draft.retrieve_set_metrics(False) + self.deck_colors = self.draft.retrieve_color_win_rate( + self.filter_format_selection.get()) + self.tier_data, tier_dict = self.draft.retrieve_tier_data( + self.tier_sources) self.main_options_dict = constants.COLUMNS_OPTIONS_MAIN_DICT.copy() self.extra_options_dict = constants.COLUMNS_OPTIONS_EXTRA_DICT.copy() for key, value in tier_dict.items(): self.main_options_dict[key] = value self.extra_options_dict[key] = value - - def UpdateDraft(self): + + def _update_draft(self): + '''Function that that triggers a search of the Arena log for draft data''' update = False - if self.draft.DraftStartSearch(): + if self.draft.draft_start_search(): update = True - self.data_sources = self.draft.RetrieveDataSources() - self.tier_sources = self.draft.RetrieveTierSource() - self.UpdateSourceOptions(True) - self.UpdateDraftData() - - if self.draft.DraftDataSearch(): + self.data_sources = self.draft.retrieve_data_sources() + self.tier_sources = self.draft.retrieve_tier_source() + self._update_data_source_options(True) + self._update_draft_data() + overlay_logger.info("%s, Mean: %.2f, Standard Deviation: %.2f", + self.draft.draft_sets, + self.set_metrics.mean, + self.set_metrics.standard_deviation) + + if self.draft.draft_data_search(): update = True return update - def UpdateSettingsStorage(self): + def _update_settings_storage(self): + '''Function that transfers settings data from the overlay widgets to a data class''' try: selection = self.column_2_selection.get() - self.configuration.column_2 = self.main_options_dict[selection] if selection in self.main_options_dict else self.main_options_dict[constants.COLUMN_2_DEFAULT] + self.configuration.column_2 = self.main_options_dict[ + selection] if selection in self.main_options_dict else self.main_options_dict[constants.COLUMN_2_DEFAULT] selection = self.column_3_selection.get() - self.configuration.column_3 = self.main_options_dict[selection] if selection in self.main_options_dict else self.main_options_dict[constants.COLUMN_3_DEFAULT] + self.configuration.column_3 = self.main_options_dict[ + selection] if selection in self.main_options_dict else self.main_options_dict[constants.COLUMN_3_DEFAULT] selection = self.column_4_selection.get() - self.configuration.column_4 = self.main_options_dict[selection] if selection in self.main_options_dict else self.main_options_dict[constants.COLUMN_4_DEFAULT] + self.configuration.column_4 = self.main_options_dict[ + selection] if selection in self.main_options_dict else self.main_options_dict[constants.COLUMN_4_DEFAULT] selection = self.column_5_selection.get() - self.configuration.column_5 = self.extra_options_dict[selection] if selection in self.extra_options_dict else self.extra_options_dict[constants.COLUMN_5_DEFAULT] + self.configuration.column_5 = self.extra_options_dict[ + selection] if selection in self.extra_options_dict else self.extra_options_dict[constants.COLUMN_5_DEFAULT] selection = self.column_6_selection.get() - self.configuration.column_6 = self.extra_options_dict[selection] if selection in self.extra_options_dict else self.extra_options_dict[constants.COLUMN_6_DEFAULT] + self.configuration.column_6 = self.extra_options_dict[ + selection] if selection in self.extra_options_dict else self.extra_options_dict[constants.COLUMN_6_DEFAULT] selection = self.column_7_selection.get() - self.configuration.column_7 = self.extra_options_dict[selection] if selection in self.extra_options_dict else self.extra_options_dict[constants.COLUMN_7_DEFAULT] + self.configuration.column_7 = self.extra_options_dict[ + selection] if selection in self.extra_options_dict else self.extra_options_dict[constants.COLUMN_7_DEFAULT] selection = self.deck_filter_selection.get() - self.configuration.deck_filter = self.deck_colors[selection] if selection in self.deck_colors else self.deck_colors[constants.DECK_FILTER_DEFAULT] + self.configuration.deck_filter = self.deck_colors[ + selection] if selection in self.deck_colors else self.deck_colors[constants.DECK_FILTER_DEFAULT] self.configuration.filter_format = self.filter_format_selection.get() self.configuration.result_format = self.result_format_selection.get() - self.configuration.missing_enabled = bool(self.missing_cards_checkbox_value.get()) - self.configuration.stats_enabled = bool(self.deck_stats_checkbox_value.get()) - self.configuration.auto_highest_enabled = bool(self.auto_highest_checkbox_value.get()) - self.configuration.curve_bonus_enabled = bool(self.curve_bonus_checkbox_value.get()) - self.configuration.color_bonus_enabled = bool(self.color_bonus_checkbox_value.get()) - self.configuration.bayesian_average_enabled = bool(self.bayesian_average_checkbox_value.get()) - self.configuration.draft_log_enabled = bool(self.draft_log_checkbox_value.get()) - self.configuration.taken_alsa_enabled = bool(self.taken_alsa_checkbox_value.get()) - self.configuration.taken_ata_enabled = bool(self.taken_ata_checkbox_value.get()) - self.configuration.taken_gpwr_enabled = bool(self.taken_gpwr_checkbox_value.get()) - self.configuration.taken_ohwr_enabled = bool(self.taken_ohwr_checkbox_value.get()) - self.configuration.taken_iwd_enabled = bool(self.taken_iwd_checkbox_value.get()) - self.configuration.taken_gndwr_enabled = bool(self.taken_gndwr_checkbox_value.get()) - self.configuration.card_colors_enabled = bool(self.card_colors_checkbox_value.get()) - CL.WriteConfig(self.configuration) + self.configuration.missing_enabled = bool( + self.missing_cards_checkbox_value.get()) + self.configuration.stats_enabled = bool( + self.deck_stats_checkbox_value.get()) + self.configuration.auto_highest_enabled = bool( + self.auto_highest_checkbox_value.get()) + self.configuration.curve_bonus_enabled = bool( + self.curve_bonus_checkbox_value.get()) + self.configuration.color_bonus_enabled = bool( + self.color_bonus_checkbox_value.get()) + self.configuration.bayesian_average_enabled = bool( + self.bayesian_average_checkbox_value.get()) + self.configuration.color_identity_enabled = bool( + self.color_identity_checkbox_value.get()) + self.configuration.draft_log_enabled = bool( + self.draft_log_checkbox_value.get()) + self.configuration.taken_alsa_enabled = bool( + self.taken_alsa_checkbox_value.get()) + self.configuration.taken_ata_enabled = bool( + self.taken_ata_checkbox_value.get()) + self.configuration.taken_gpwr_enabled = bool( + self.taken_gpwr_checkbox_value.get()) + self.configuration.taken_ohwr_enabled = bool( + self.taken_ohwr_checkbox_value.get()) + self.configuration.taken_iwd_enabled = bool( + self.taken_iwd_checkbox_value.get()) + self.configuration.taken_gndwr_enabled = bool( + self.taken_gndwr_checkbox_value.get()) + self.configuration.taken_gdwr_enabled = bool( + self.taken_gdwr_checkbox_value.get()) + self.configuration.card_colors_enabled = bool( + self.card_colors_checkbox_value.get()) + CL.write_config(self.configuration) except Exception as error: - overlay_logger.info(f"UpdateSettingsStorage Error: {error}") - - def UpdateSettingsData(self): - self.ControlTrace(False) + overlay_logger.info("__update_settings_storage Error: %s", error) + + def _update_settings_data(self): + '''Function that transfers settings data from a data class to the overlay widgets''' + self._control_trace(False) try: - selection = [k for k,v in self.main_options_dict.items() if v == self.configuration.column_2] - self.column_2_selection.set(selection[0] if len(selection) else constants.COLUMN_2_DEFAULT) - selection = [k for k,v in self.main_options_dict.items() if v == self.configuration.column_3] - self.column_3_selection.set(selection[0] if len(selection) else constants.COLUMN_3_DEFAULT) - selection = [k for k,v in self.main_options_dict.items() if v == self.configuration.column_4] - self.column_4_selection.set(selection[0] if len(selection) else constants.COLUMN_4_DEFAULT) - selection = [k for k,v in self.extra_options_dict.items() if v == self.configuration.column_5] - self.column_5_selection.set(selection[0] if len(selection) else constants.COLUMN_5_DEFAULT) - selection = [k for k,v in self.extra_options_dict.items() if v == self.configuration.column_6] - self.column_6_selection.set(selection[0] if len(selection) else constants.COLUMN_6_DEFAULT) - selection = [k for k,v in self.extra_options_dict.items() if v == self.configuration.column_7] - self.column_7_selection.set(selection[0] if len(selection) else constants.COLUMN_7_DEFAULT) - selection = [k for k,v in self.deck_colors.items() if v == self.configuration.deck_filter] - self.deck_filter_selection.set(selection[0] if len(selection) else constants.DECK_FILTER_DEFAULT) + selection = [k for k, v in self.main_options_dict.items( + ) if v == self.configuration.column_2] + self.column_2_selection.set(selection[0] if len( + selection) else constants.COLUMN_2_DEFAULT) + selection = [k for k, v in self.main_options_dict.items( + ) if v == self.configuration.column_3] + self.column_3_selection.set(selection[0] if len( + selection) else constants.COLUMN_3_DEFAULT) + selection = [k for k, v in self.main_options_dict.items( + ) if v == self.configuration.column_4] + self.column_4_selection.set(selection[0] if len( + selection) else constants.COLUMN_4_DEFAULT) + selection = [k for k, v in self.extra_options_dict.items( + ) if v == self.configuration.column_5] + self.column_5_selection.set(selection[0] if len( + selection) else constants.COLUMN_5_DEFAULT) + selection = [k for k, v in self.extra_options_dict.items( + ) if v == self.configuration.column_6] + self.column_6_selection.set(selection[0] if len( + selection) else constants.COLUMN_6_DEFAULT) + selection = [k for k, v in self.extra_options_dict.items( + ) if v == self.configuration.column_7] + self.column_7_selection.set(selection[0] if len( + selection) else constants.COLUMN_7_DEFAULT) + selection = [k for k, v in self.deck_colors.items( + ) if v == self.configuration.deck_filter] + self.deck_filter_selection.set(selection[0] if len( + selection) else constants.DECK_FILTER_DEFAULT) self.filter_format_selection.set(self.configuration.filter_format) self.result_format_selection.set(self.configuration.result_format) - self.deck_stats_checkbox_value.set(self.configuration.stats_enabled) - self.missing_cards_checkbox_value.set(self.configuration.missing_enabled) - self.auto_highest_checkbox_value.set(self.configuration.auto_highest_enabled) - self.curve_bonus_checkbox_value.set(self.configuration.curve_bonus_enabled) - self.color_bonus_checkbox_value.set(self.configuration.color_bonus_enabled) - self.bayesian_average_checkbox_value.set(self.configuration.bayesian_average_enabled) - self.draft_log_checkbox_value.set(self.configuration.draft_log_enabled) - self.taken_alsa_checkbox_value.set(self.configuration.taken_alsa_enabled) - self.taken_ata_checkbox_value.set(self.configuration.taken_ata_enabled) - self.taken_gpwr_checkbox_value.set(self.configuration.taken_gpwr_enabled) - self.taken_ohwr_checkbox_value.set(self.configuration.taken_ohwr_enabled) - self.taken_gndwr_checkbox_value.set(self.configuration.taken_gndwr_enabled) - self.taken_iwd_checkbox_value.set(self.configuration.taken_iwd_enabled) - self.card_colors_checkbox_value.set(self.configuration.card_colors_enabled) + self.deck_stats_checkbox_value.set( + self.configuration.stats_enabled) + self.missing_cards_checkbox_value.set( + self.configuration.missing_enabled) + self.auto_highest_checkbox_value.set( + self.configuration.auto_highest_enabled) + self.curve_bonus_checkbox_value.set( + self.configuration.curve_bonus_enabled) + self.color_bonus_checkbox_value.set( + self.configuration.color_bonus_enabled) + self.bayesian_average_checkbox_value.set( + self.configuration.bayesian_average_enabled) + self.color_identity_checkbox_value.set( + self.configuration.color_identity_enabled) + self.draft_log_checkbox_value.set( + self.configuration.draft_log_enabled) + self.taken_alsa_checkbox_value.set( + self.configuration.taken_alsa_enabled) + self.taken_ata_checkbox_value.set( + self.configuration.taken_ata_enabled) + self.taken_gpwr_checkbox_value.set( + self.configuration.taken_gpwr_enabled) + self.taken_ohwr_checkbox_value.set( + self.configuration.taken_ohwr_enabled) + self.taken_gdwr_checkbox_value.set( + self.configuration.taken_gdwr_enabled) + self.taken_gndwr_checkbox_value.set( + self.configuration.taken_gndwr_enabled) + self.taken_iwd_checkbox_value.set( + self.configuration.taken_iwd_enabled) + self.card_colors_checkbox_value.set( + self.configuration.card_colors_enabled) except Exception as error: - self.column_2_selection.set(constants.COLUMN_2_DEFAULT) + self.column_2_selection.set(constants.COLUMN_2_DEFAULT) self.column_3_selection.set(constants.COLUMN_3_DEFAULT) self.column_4_selection.set(constants.COLUMN_4_DEFAULT) self.column_5_selection.set(constants.COLUMN_5_DEFAULT) @@ -971,479 +1324,639 @@ def UpdateSettingsData(self): self.auto_highest_checkbox_value.set(False) self.curve_bonus_checkbox_value.set(False) self.color_bonus_checkbox_value.set(False) + self.color_identity_checkbox_value.set(False) self.bayesian_average_checkbox_value.set(False) self.draft_log_checkbox_value.set(False) self.taken_alsa_checkbox_value.set(True) self.taken_ata_checkbox_value.set(True) self.taken_gpwr_checkbox_value.set(True) self.taken_ohwr_checkbox_value.set(True) + self.taken_gdwr_checkbox_value.set(True) self.taken_gndwr_checkbox_value.set(True) self.taken_iwd_checkbox_value.set(True) self.card_colors_checkbox_value.set(True) - overlay_logger.info(f"UpdateSettingsData Error: {error}") - self.ControlTrace(True) - - self.draft.LogEnable(self.configuration.draft_log_enabled) - - def InitializeUI(self): - self.UpdateSourceOptions(False) - self.UpdateColumnOptions() - - self.EnableDeckStates(self.deck_stats_checkbox_value.get()) - self.EnableMissingCards(self.missing_cards_checkbox_value.get()) - self.UpdateCurrentDraft(self.draft.draft_sets, self.draft.draft_type) - self.UpdatePackPick(self.draft.current_pack, self.draft.current_pick) - - fields = {"Column1" : constants.DATA_FIELD_NAME, - "Column2" : self.main_options_dict[self.column_2_selection.get()], - "Column3" : self.main_options_dict[self.column_3_selection.get()], - "Column4" : self.main_options_dict[self.column_4_selection.get()], - "Column5" : self.extra_options_dict[self.column_5_selection.get()], - "Column6" : self.extra_options_dict[self.column_6_selection.get()], - "Column7" : self.extra_options_dict[self.column_7_selection.get()],} - self.UpdatePackTable([], [], self.deck_filter_selection.get(), fields) - - self.UpdateMissingTable([],[],[],[],self.deck_filter_selection.get(),fields) - - self.UpdateDeckStatsCallback() + overlay_logger.info("__update_settings_data Error: %s", error) + self._control_trace(True) + + self.draft.log_enable(self.configuration.draft_log_enabled) + + def _initialize_overlay_widgets(self): + '''Set the overlay widgets in the main window to a known state at startup''' + self._update_data_source_options(False) + self._update_column_options() + + self._enable_deck_stats_table(self.deck_stats_checkbox_value.get()) + self._enable_missing_cards_table( + self.missing_cards_checkbox_value.get()) + self._update_current_draft_label( + self.draft.draft_sets, self.draft.draft_type) + self._update_pack_pick_label( + self.draft.current_pack, self.draft.current_pick) + + fields = {"Column1": constants.DATA_FIELD_NAME, + "Column2": self.main_options_dict[self.column_2_selection.get()], + "Column3": self.main_options_dict[self.column_3_selection.get()], + "Column4": self.main_options_dict[self.column_4_selection.get()], + "Column5": self.extra_options_dict[self.column_5_selection.get()], + "Column6": self.extra_options_dict[self.column_6_selection.get()], + "Column7": self.extra_options_dict[self.column_7_selection.get()], } + self._update_pack_table([], self.deck_filter_selection.get(), fields) + + self._update_missing_table( + [], [], [], self.deck_filter_selection.get(), fields) self.root.update() - def UpdateCallback(self, enable_draft_search): + self._update_deck_stats_callback() + + self.root.update() + + def _update_overlay_callback(self, enable_draft_search): + '''Callback function that updates all of the widgets in the main window''' update = True if enable_draft_search: - update = self.UpdateDraft() - + update = self._update_draft() + if not update: return - self.UpdateSourceOptions(False) - self.UpdateColumnOptions() - - self.EnableDeckStates(self.deck_stats_checkbox_value.get()) - self.EnableMissingCards(self.missing_cards_checkbox_value.get()) - - taken_cards = self.draft.TakenCards() - - filtered = self.DeckFilterColors(taken_cards, self.deck_filter_selection.get()) - fields = {"Column1" : constants.DATA_FIELD_NAME, - "Column2" : self.main_options_dict[self.column_2_selection.get()], - "Column3" : self.main_options_dict[self.column_3_selection.get()], - "Column4" : self.main_options_dict[self.column_4_selection.get()], - "Column5" : self.extra_options_dict[self.column_5_selection.get()], - "Column6" : self.extra_options_dict[self.column_6_selection.get()], - "Column7" : self.extra_options_dict[self.column_7_selection.get()],} - - self.UpdateCurrentDraft(self.draft.draft_sets, self.draft.draft_type) - self.UpdatePackPick(self.draft.current_pack, self.draft.current_pick) + self._update_data_source_options(False) + self._update_column_options() + + self._enable_deck_stats_table(self.deck_stats_checkbox_value.get()) + self._enable_missing_cards_table( + self.missing_cards_checkbox_value.get()) + + taken_cards = self.draft.retrieve_taken_cards() + + filtered = self._identify_auto_colors( + taken_cards, self.deck_filter_selection.get()) + fields = {"Column1": constants.DATA_FIELD_NAME, + "Column2": self.main_options_dict[self.column_2_selection.get()], + "Column3": self.main_options_dict[self.column_3_selection.get()], + "Column4": self.main_options_dict[self.column_4_selection.get()], + "Column5": self.extra_options_dict[self.column_5_selection.get()], + "Column6": self.extra_options_dict[self.column_6_selection.get()], + "Column7": self.extra_options_dict[self.column_7_selection.get()], } + + self._update_current_draft_label( + self.draft.draft_sets, self.draft.draft_type) + self._update_pack_pick_label( + self.draft.current_pack, self.draft.current_pick) pack_index = (self.draft.current_pick - 1) % 8 - pack_cards = self.draft.PackCards(pack_index) - self.UpdatePackTable(pack_cards, - taken_cards, - filtered, - fields) - - self.UpdateMissingTable(pack_cards, - self.draft.InitialPackCards(pack_index), - self.draft.PickedCards(pack_index), - taken_cards, + pack_cards = self.draft.retrieve_pack_cards(pack_index) + self._update_pack_table(pack_cards, filtered, - fields) - - self.UpdateDeckStatsCallback() - self.UpdateTakenTable() - - def UpdateDeckStatsCallback(self, *args): - self.root.update_idletasks() - self.UpdateDeckStatsTable(self.draft.TakenCards(), self.stat_options_selection.get(), self.pack_table.winfo_width()) - - def UpdateUI(self): + fields) + + self._update_missing_table(pack_cards, + self.draft.retrieve_initial_pack_cards( + pack_index), + self.draft.retrieve_picked_cards( + pack_index), + filtered, + fields) + + self._update_deck_stats_callback() + self._update_taken_table() + + def _update_deck_stats_callback(self, *args): + '''Callback function that updates the Deck Stats table in the main window''' + self.root.update_idletasks() + self._update_deck_stats_table(self.draft.retrieve_taken_cards( + ), self.stat_options_selection.get(), self.pack_table.winfo_width()) + + def _arena_log_check(self): + '''Function that monitors the Arena log every 1000ms to determine if there's new draft data''' try: self.current_timestamp = os.stat(self.arena_file).st_mtime - + if self.current_timestamp != self.previous_timestamp: self.previous_timestamp = self.current_timestamp - - while(True): - self.UpdateCallback(True) + while True: + + self._update_overlay_callback(True) if self.draft.step_through: input("Continue?") else: break except Exception as error: - overlay_logger.info(f"UpdateUI Error: {error}") - self.DraftReset(True) - - self.root.after(1000, self.UpdateUI) - - def WindowLift(self): - if self.root.state()=="iconic": + overlay_logger.info("__arena_log_check Error: %s", error) + self._reset_draft(True) + + self.root.after(1000, self._arena_log_check) + + def lift_window(self): + '''Function that's used to minimize a window or set it as the top most window''' + if self.root.state() == "iconic": self.root.deiconify() self.root.lift() self.root.attributes("-topmost", True) else: self.root.attributes("-topmost", False) self.root.iconify() - - def UpdateSetStartDate(self, start, selection, set_list, *args): + + def _update_set_start_date(self, start, selection, set_list, *args): + '''Function that's used to determine if a set in the Set View window has minimum start date + Example: The user shouldn't download Arena Cube data that's more than a couple of months old + or else they risk downloading data from multiple separate cubes + ''' try: set_data = set_list[selection.get()] - + if constants.SET_START_DATE in set_data: - start.delete(0,END) - start.insert(END, set_data[constants.SET_START_DATE]) - + start.delete(0, tkinter.END) + start.insert(tkinter.END, set_data[constants.SET_START_DATE]) + self.root.update() except Exception as error: - overlay_logger.info(f"UpdateSetStartDate Error: {error}") - - def SetViewPopup(self): - popup = Toplevel() + overlay_logger.info("__update_set_start_date Error: %s", error) + + def _open_set_view_window(self): + '''Creates the Set View window''' + popup = tkinter.Toplevel() popup.wm_title("Set Data") - popup.resizable(width = False, height = True) + popup.resizable(width=False, height=True) popup.attributes("-topmost", True) - x, y = SafeCoordinates(self.root, 1000, 170, 250, 20) - popup.wm_geometry("+%d+%d" % (x, y)) - - Grid.rowconfigure(popup, 1, weight = 1) + location_x, location_y = identify_safe_coordinates(self.root, + self._scale_value( + 1000), + self._scale_value( + 170), + self._scale_value( + 250), + self._scale_value(20)) + popup.wm_geometry(f"+{location_x}+{location_y}") + + tkinter.Grid.rowconfigure(popup, 1, weight=1) try: - sets = self.extractor.SetList() - - headers = {"SET" : {"width" : .40, "anchor" : W}, - "DRAFT" : {"width" : .20, "anchor" : CENTER}, - "START DATE" : {"width" : .20, "anchor" : CENTER}, - "END DATE" : {"width" : .20, "anchor" : CENTER}} - - style = Style() - style.configure("Set.Treeview", rowheight=25) - - list_box_frame = Frame(popup) - list_box_scrollbar = Scrollbar(list_box_frame, orient=VERTICAL) - list_box_scrollbar.pack(side=RIGHT, fill=Y) - - list_box = CreateHeader(list_box_frame, 0, 10, headers, 500, True, True, "Set.Treeview", True) - + sets = self.extractor.return_set_list() + + headers = {"SET": {"width": .40, "anchor": tkinter.W}, + "DRAFT": {"width": .20, "anchor": tkinter.CENTER}, + "START DATE": {"width": .20, "anchor": tkinter.CENTER}, + "END DATE": {"width": .20, "anchor": tkinter.CENTER}} + + #style = Style() + #style.configure("Set.Treeview", rowheight=self._scale_value(25)) + + list_box_frame = tkinter.Frame(popup) + list_box_scrollbar = tkinter.Scrollbar( + list_box_frame, orient=tkinter.VERTICAL) + list_box_scrollbar.pack(side=tkinter.RIGHT, fill=tkinter.Y) + + list_box = create_header( + list_box_frame, 0, self.fonts_dict["Sets.TableRow"], headers, self._scale_value(500), True, True, "Set.Treeview", True) + list_box.config(yscrollcommand=list_box_scrollbar.set) list_box_scrollbar.config(command=list_box.yview) - - notice_label = Label(popup, text="17Lands has an embargo period of 12 days for new sets on Magic Arena. Visit https://www.17lands.com for more details.", font=f'{constants.FONT_SANS_SERIF} 9', anchor="c") - set_label = Label(popup, text="Set:", font=f'{constants.FONT_SANS_SERIF} 10 bold') - draft_label = Label(popup, text="Draft:", font=f'{constants.FONT_SANS_SERIF} 10 bold') - start_label = Label(popup, text="Start Date:", font=f'{constants.FONT_SANS_SERIF} 10 bold') - end_label = Label(popup, text="End Date:", font=f'{constants.FONT_SANS_SERIF} 10 bold') + + notice_label = Label(popup, text="17Lands has an embargo period of 12 days for new sets on Magic Arena. Visit https://www.17lands.com for more details.", + anchor="c") + set_label = Label(popup, text="Set:", + style="SetOptions.TLabel") + draft_label = Label(popup, text="Draft:", + style="SetOptions.TLabel") + start_label = Label(popup, text="Start Date:", + style="SetOptions.TLabel") + end_label = Label(popup, text="End Date:", + style="SetOptions.TLabel") draft_choices = constants.LIMITED_TYPE_LIST - status_text = StringVar() - status_label = Label(popup, textvariable=status_text, font=f'{constants.FONT_SANS_SERIF} 12 bold', anchor="c") - - draft_value = StringVar(self.root) - draft_entry = OptionMenu(popup, draft_value, draft_choices[0], *draft_choices) - - start_entry = Entry(popup) - start_entry.insert(END, constants.SET_START_DATE_DEFAULT) - end_entry = Entry(popup) - end_entry.insert(END, str(date.today())) - + status_text = tkinter.StringVar() + status_label = Label(popup, textvariable=status_text, + style="Status.TLabel", anchor="c") + status_text.set("Retrieving Set List") + + draft_value = tkinter.StringVar(self.root) + draft_entry = OptionMenu( + popup, draft_value, draft_choices[0], *draft_choices) + menu = self.root.nametowidget(draft_entry['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) + + start_entry = tkinter.Entry(popup) + start_entry.insert(tkinter.END, constants.SET_START_DATE_DEFAULT) + end_entry = tkinter.Entry(popup) + end_entry.insert(tkinter.END, str(date.today())) + set_choices = list(sets.keys()) - - set_value = StringVar(self.root) - set_entry = OptionMenu(popup, set_value, set_choices[0], *set_choices) - set_value.trace("w", lambda *args, start=start_entry, selection=set_value, set_list=sets : self.UpdateSetStartDate(start, selection, set_list, *args)) - - progress = Progressbar(popup,orient=HORIZONTAL,length=100,mode='determinate') - - add_button = Button(popup, command=lambda: self.AddSet(popup, - set_value, - draft_value, - start_entry, - end_entry, - add_button, - progress, - list_box, - sets, - status_text, - constants.DATA_SET_VERSION_3), text="ADD SET") - - - notice_label.grid(row=0, column=0, columnspan=8, sticky = 'nsew') - list_box_frame.grid(row=1, column=0, columnspan=8, sticky = 'nsew') - set_label.grid(row=2, column=0, sticky = 'nsew') - set_entry.grid(row=2, column=1, sticky = 'nsew') - start_label.grid(row=2, column=2, sticky = 'nsew') - start_entry.grid(row=2, column=3, sticky = 'nsew') - end_label.grid(row=2, column=4, sticky = 'nsew') - end_entry.grid(row=2, column=5, sticky = 'nsew') - draft_label.grid(row=2, column=6, sticky = 'nsew') - draft_entry.grid(row=2, column=7, sticky = 'nsew') - add_button.grid(row=3, column=0, columnspan=8, sticky = 'nsew') - progress.grid(row=4, column=0, columnspan=8, sticky = 'nsew') - status_label.grid(row=5, column=0, columnspan=8, sticky = 'nsew') - - list_box.pack(expand = True, fill = "both") - - self.DataViewUpdate(list_box, sets) + + set_value = tkinter.StringVar(self.root) + set_entry = OptionMenu( + popup, set_value, set_choices[0], *set_choices) + menu = self.root.nametowidget(set_entry['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) + + set_value.trace("w", lambda *args, start=start_entry, selection=set_value, + set_list=sets: self._update_set_start_date(start, selection, set_list, *args)) + + progress = Progressbar( + popup, orient=tkinter.HORIZONTAL, length=100, mode='determinate') + + add_button = Button(popup, command=lambda: self._add_set(popup, + set_value, + draft_value, + start_entry, + end_entry, + add_button, + progress, + list_box, + sets, + status_text, + constants.DATA_SET_VERSION_3), text="ADD SET") + + notice_label.grid(row=0, column=0, columnspan=8, sticky='nsew') + list_box_frame.grid(row=1, column=0, columnspan=8, sticky='nsew') + set_label.grid(row=2, column=0, sticky='nsew') + set_entry.grid(row=2, column=1, sticky='nsew') + start_label.grid(row=2, column=2, sticky='nsew') + start_entry.grid(row=2, column=3, sticky='nsew') + end_label.grid(row=2, column=4, sticky='nsew') + end_entry.grid(row=2, column=5, sticky='nsew') + draft_label.grid(row=2, column=6, sticky='nsew') + draft_entry.grid(row=2, column=7, sticky='nsew') + add_button.grid(row=3, column=0, columnspan=8, sticky='nsew') + progress.grid(row=4, column=0, columnspan=8, sticky='nsew') + status_label.grid(row=5, column=0, columnspan=8, sticky='nsew') + + list_box.pack(expand=True, fill="both") + + self._update_set_table(list_box, sets) + status_text.set("") + popup.update() except Exception as error: - overlay_logger.info(f"SetViewPopup Error: {error}") - - def CardComparePopup(self): - popup = Toplevel() + overlay_logger.info("__open_set_view_window Error: %s", error) + + def _open_card_compare_window(self): + '''Creates the Card Compare window''' + popup = tkinter.Toplevel() popup.wm_title("Card Compare") - popup.resizable(width = False, height = True) + popup.resizable(width=False, height=True) popup.attributes("-topmost", True) - x, y = SafeCoordinates(self.root, 400, 170, 250, 0) - popup.wm_geometry("+%d+%d" % (x, y)) - + location_x, location_y = identify_safe_coordinates(self.root, + self._scale_value( + 400), + self._scale_value( + 170), + self._scale_value( + 250), + self._scale_value(0)) + popup.wm_geometry(f"+{location_x}+{location_y}") + try: - Grid.rowconfigure(popup, 2, weight = 1) - Grid.columnconfigure(popup, 0, weight = 1) - - taken_cards = self.draft.TakenCards() - - filtered = self.DeckFilterColors(taken_cards, self.deck_filter_selection.get()) - fields = {"Column1" : constants.DATA_FIELD_NAME, - "Column2" : self.main_options_dict[self.column_2_selection.get()], - "Column3" : self.main_options_dict[self.column_3_selection.get()], - "Column4" : self.main_options_dict[self.column_4_selection.get()], - "Column5" : self.extra_options_dict[self.column_5_selection.get()], - "Column6" : self.extra_options_dict[self.column_6_selection.get()], - "Column7" : self.extra_options_dict[self.column_7_selection.get()],} - + tkinter.Grid.rowconfigure(popup, 2, weight=1) + tkinter.Grid.columnconfigure(popup, 0, weight=1) + + taken_cards = self.draft.retrieve_taken_cards() + + filtered = self._identify_auto_colors( + taken_cards, self.deck_filter_selection.get()) + fields = {"Column1": constants.DATA_FIELD_NAME, + "Column2": self.main_options_dict[self.column_2_selection.get()], + "Column3": self.main_options_dict[self.column_3_selection.get()], + "Column4": self.main_options_dict[self.column_4_selection.get()], + "Column5": self.extra_options_dict[self.column_5_selection.get()], + "Column6": self.extra_options_dict[self.column_6_selection.get()], + "Column7": self.extra_options_dict[self.column_7_selection.get()], } + matching_cards = [] - - card_frame = Frame(popup) - set_card_names = [v[constants.DATA_FIELD_NAME] for k,v in self.draft.set_data["card_ratings"].items()] + card_frame = tkinter.Frame(popup) + + set_card_names = [v[constants.DATA_FIELD_NAME] + for k, v in self.draft.set_data["card_ratings"].items()] card_entry = AutocompleteEntry( - card_frame, - completevalues=set_card_names - ) - - headers = {"Column1" : {"width" : .46, "anchor" : W}, - "Column2" : {"width" : .18, "anchor" : CENTER}, - "Column3" : {"width" : .18, "anchor" : CENTER}, - "Column4" : {"width" : .18, "anchor" : CENTER}, - "Column5" : {"width" : .18, "anchor" : CENTER}, - "Column6" : {"width" : .18, "anchor" : CENTER}, - "Column7" : {"width" : .18, "anchor" : CENTER}} - - compare_table_frame = Frame(popup) - compare_scrollbar = Scrollbar(compare_table_frame, orient=VERTICAL) - compare_scrollbar.pack(side=RIGHT, fill=Y) - compare_table = CreateHeader(compare_table_frame, 0, 8, headers, self.configuration.table_width, True, True, constants.TABLE_STYLE, False) + card_frame, + completevalues=set_card_names + ) + + headers = {"Column1": {"width": .46, "anchor": tkinter.W}, + "Column2": {"width": .18, "anchor": tkinter.CENTER}, + "Column3": {"width": .18, "anchor": tkinter.CENTER}, + "Column4": {"width": .18, "anchor": tkinter.CENTER}, + "Column5": {"width": .18, "anchor": tkinter.CENTER}, + "Column6": {"width": .18, "anchor": tkinter.CENTER}, + "Column7": {"width": .18, "anchor": tkinter.CENTER}} + + compare_table_frame = tkinter.Frame(popup) + compare_scrollbar = tkinter.Scrollbar( + compare_table_frame, orient=tkinter.VERTICAL) + compare_scrollbar.pack(side=tkinter.RIGHT, fill=tkinter.Y) + compare_table = create_header(compare_table_frame, 0, self.fonts_dict["All.TableRow"], headers, + self.configuration.table_width, True, True, constants.TABLE_STYLE, False) compare_table.config(yscrollcommand=compare_scrollbar.set) compare_scrollbar.config(command=compare_table.yview) - - clear_button = Button(popup, text="Clear", command=lambda:self.ClearCompareTable(compare_table, matching_cards)) + + clear_button = Button(popup, text="Clear", command=lambda: self._clear_compare_table( + compare_table, matching_cards)) card_frame.grid(row=0, column=0, sticky="nsew") - clear_button.grid(row=1, column=0, sticky= "nsew") + clear_button.grid(row=1, column=0, sticky="nsew") compare_table_frame.grid(row=2, column=0, sticky="nsew") - - compare_table.pack(expand = True, fill = "both") - card_entry.pack(side = LEFT, expand = True, fill = "both") - - card_entry.bind("", lambda event: self.UpdateCompareTable(compare_table, - matching_cards, - card_entry, - self.draft.set_data["card_ratings"], - filtered, - fields)) - - self.UpdateCompareTable(compare_table, - matching_cards, - card_entry, - self.draft.set_data["card_ratings"], - filtered, - fields) - + + compare_table.pack(expand=True, fill="both") + card_entry.pack(side=tkinter.LEFT, expand=True, fill="both") + + card_entry.bind("", lambda event: self._update_compare_table(compare_table, + matching_cards, + card_entry, + self.draft.set_data["card_ratings"], + filtered, + fields)) + + self._update_compare_table(compare_table, + matching_cards, + card_entry, + self.draft.set_data["card_ratings"], + filtered, + fields) + except Exception as error: - overlay_logger.info(f"CardComparePopup Error: {error}") + overlay_logger.info("__open_card_compare_window Error: %s", error) - def TakenCardsExit(self, popup): + def _close_taken_cards_window(self, popup): + '''Clear taken card table data when the Taken Cards window is closed''' self.taken_table = None - - popup.destroy() - def TakenCardsPopup(self): - popup = Toplevel() + popup.destroy() + + def _open_taken_cards_window(self): + '''Creates the Taken Cards window''' + popup = tkinter.Toplevel() popup.wm_title("Taken Cards") popup.attributes("-topmost", True) - popup.resizable(width = False, height = True) - x, y = SafeCoordinates(self.root, 400, 170, 250, 0) - popup.wm_geometry("+%d+%d" % (x, y)) - - popup.protocol("WM_DELETE_WINDOW", lambda window=popup: self.TakenCardsExit(window)) + popup.resizable(width=False, height=True) + location_x, location_y = identify_safe_coordinates(self.root, + self._scale_value( + 400), + self._scale_value( + 170), + self._scale_value( + 250), + self._scale_value(0)) + popup.wm_geometry(f"+{location_x}+{location_y}") + + popup.protocol( + "WM_DELETE_WINDOW", lambda window=popup: self._close_taken_cards_window(window)) + self._control_trace(False) try: - Grid.rowconfigure(popup, 3, weight = 1) - Grid.columnconfigure(popup, 6, weight = 1) - - taken_cards = self.draft.TakenCards() - copy_button = Button(popup, command=lambda:CopyTaken(taken_cards, - self.draft.set_data), - text="Copy to Clipboard") - - headers = {"Column1" : {"width" : .40, "anchor" : W}, - "Column2" : {"width" : .20, "anchor" : CENTER}, - "Column3" : {"width" : .20, "anchor" : CENTER}, - "Column4" : {"width" : .20, "anchor" : CENTER}, - "Column5" : {"width" : .20, "anchor" : CENTER}, - "Column6" : {"width" : .20, "anchor" : CENTER}, - "Column7" : {"width" : .20, "anchor" : CENTER}, - "Column8" : {"width" : .20, "anchor" : CENTER}, - "Column9" : {"width" : .20, "anchor" : CENTER}, - "Column10": {"width" : .20, "anchor" : CENTER}, - } - - style = Style() - style.configure("Taken.Treeview", rowheight=25) - - taken_table_frame = Frame(popup) - taken_scrollbar = Scrollbar(taken_table_frame, orient=VERTICAL) - taken_scrollbar.pack(side=RIGHT, fill=Y) - self.taken_table = CreateHeader(taken_table_frame, 0, 8, headers, 410, True, True, "Taken.Treeview", False) + tkinter.Grid.rowconfigure(popup, 4, weight=1) + tkinter.Grid.columnconfigure(popup, 6, weight=1) + + taken_cards = self.draft.retrieve_taken_cards() + copy_button = Button(popup, command=lambda: copy_taken(taken_cards), + text="Copy to Clipboard") + + headers = {"Column1": {"width": .40, "anchor": tkinter.W}, + "Column2": {"width": .20, "anchor": tkinter.CENTER}, + "Column3": {"width": .20, "anchor": tkinter.CENTER}, + "Column4": {"width": .20, "anchor": tkinter.CENTER}, + "Column5": {"width": .20, "anchor": tkinter.CENTER}, + "Column6": {"width": .20, "anchor": tkinter.CENTER}, + "Column7": {"width": .20, "anchor": tkinter.CENTER}, + "Column8": {"width": .20, "anchor": tkinter.CENTER}, + "Column9": {"width": .20, "anchor": tkinter.CENTER}, + "Column10": {"width": .20, "anchor": tkinter.CENTER}, + "Column11": {"width": .20, "anchor": tkinter.CENTER}, + } + + taken_table_frame = tkinter.Frame(popup) + taken_scrollbar = tkinter.Scrollbar( + taken_table_frame, orient=tkinter.VERTICAL) + taken_scrollbar.pack(side=tkinter.RIGHT, fill=tkinter.Y) + self.taken_table = create_header( + taken_table_frame, 0, self.fonts_dict["All.TableRow"], headers, self._scale_value(440), True, True, "Taken.Treeview", False) self.taken_table.config(yscrollcommand=taken_scrollbar.set) taken_scrollbar.config(command=self.taken_table.yview) - - option_frame = Frame(popup) - taken_filter_label = Label(option_frame, text="Deck Filter:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") + + option_frame = tkinter.Frame( + popup, highlightbackground="white", highlightthickness=2) + taken_filter_label = Label( + option_frame, text="Deck Filter:", style="MainSections.TLabel", anchor="w") self.taken_filter_selection.set(self.deck_filter_selection.get()) taken_filter_list = self.deck_filter_list - - taken_option = OptionMenu(option_frame, self.taken_filter_selection, self.taken_filter_selection.get(), *taken_filter_list, style="my.TMenubutton") - - #type_label = Label(option_frame, text="Type Filter:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") - #self.taken_type_selection.set(next(iter(constants.CARD_TYPE_DICT))) - #taken_type_list = constants.CARD_TYPE_DICT.keys() - # - #type_option = OptionMenu(option_frame, self.taken_type_selection, self.taken_type_selection.get(), *taken_type_list, style="my.TMenubutton") - - checkbox_frame = Frame(popup) + + taken_option = OptionMenu(option_frame, self.taken_filter_selection, self.taken_filter_selection.get( + ), *taken_filter_list, style="All.TMenubutton") + menu = self.root.nametowidget(taken_option['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) + + type_checkbox_frame = tkinter.Frame( + popup, highlightbackground="white", highlightthickness=2) + + taken_creature_checkbox = Checkbutton(type_checkbox_frame, + text="CREATURES", + style="Taken.TCheckbutton", + variable=self.taken_type_creature_checkbox_value, + onvalue=1, + offvalue=0) + + taken_land_checkbox = Checkbutton(type_checkbox_frame, + text="LANDS", + style="Taken.TCheckbutton", + variable=self.taken_type_land_checkbox_value, + onvalue=1, + offvalue=0) + + taken_instant_sorcery_checkbox = Checkbutton(type_checkbox_frame, + text="INSTANTS/SORCERIES", + style="Taken.TCheckbutton", + variable=self.taken_type_instant_sorcery_checkbox_value, + onvalue=1, + offvalue=0) + + taken_other_checkbox = Checkbutton(type_checkbox_frame, + text="OTHER", + style="Taken.TCheckbutton", + variable=self.taken_type_other_checkbox_value, + onvalue=1, + offvalue=0) + + checkbox_frame = tkinter.Frame( + popup, highlightbackground="white", highlightthickness=2) + taken_alsa_checkbox = Checkbutton(checkbox_frame, - text = "ALSA", + text="ALSA", + style="Taken.TCheckbutton", variable=self.taken_alsa_checkbox_value, onvalue=1, - offvalue=0) + offvalue=0) taken_ata_checkbox = Checkbutton(checkbox_frame, - text = "ATA", - variable=self.taken_ata_checkbox_value, - onvalue=1, - offvalue=0) + text="ATA", + style="Taken.TCheckbutton", + variable=self.taken_ata_checkbox_value, + onvalue=1, + offvalue=0) taken_gpwr_checkbox = Checkbutton(checkbox_frame, - text = "GPWR", + text="GPWR", + style="Taken.TCheckbutton", variable=self.taken_gpwr_checkbox_value, onvalue=1, - offvalue=0) + offvalue=0) taken_ohwr_checkbox = Checkbutton(checkbox_frame, - text = "OHWR", + text="OHWR", + style="Taken.TCheckbutton", variable=self.taken_ohwr_checkbox_value, onvalue=1, - offvalue=0) - taken_gndwr_checkbox = Checkbutton(checkbox_frame, - text = "GNDWR", - variable=self.taken_gndwr_checkbox_value, + offvalue=0) + taken_gdwr_checkbox = Checkbutton(checkbox_frame, + text="GDWR", + style="Taken.TCheckbutton", + variable=self.taken_gdwr_checkbox_value, onvalue=1, - offvalue=0) + offvalue=0) + taken_gndwr_checkbox = Checkbutton(checkbox_frame, + text="GNDWR", + style="Taken.TCheckbutton", + variable=self.taken_gndwr_checkbox_value, + onvalue=1, + offvalue=0) taken_iwd_checkbox = Checkbutton(checkbox_frame, - text = "IWD", - variable=self.taken_iwd_checkbox_value, - onvalue=1, - offvalue=0) + text="IWD", + style="Taken.TCheckbutton", + variable=self.taken_iwd_checkbox_value, + onvalue=1, + offvalue=0) option_frame.grid(row=0, column=0, columnspan=7, sticky="nsew") - checkbox_frame.grid(row=1, column=0, columnspan = 7, sticky="nsew") - copy_button.grid(row=2, column=0, columnspan = 7, sticky="nsew") - taken_table_frame.grid(row=3, column=0, columnspan = 7, sticky = "nsew") - self.taken_table.pack(side=LEFT, expand = True, fill = "both") - taken_alsa_checkbox.pack(side=LEFT, expand = True, fill = "both") - taken_ata_checkbox.pack(side=LEFT, expand = True, fill = "both") - taken_gpwr_checkbox.pack(side=LEFT, expand = True, fill = "both") - taken_ohwr_checkbox.pack(side=LEFT, expand = True, fill = "both") - taken_gndwr_checkbox.pack(side=LEFT, expand = True, fill = "both") - taken_iwd_checkbox.pack(side=LEFT, expand = True, fill = "both") - - taken_filter_label.pack(side=LEFT, expand = True, fill = None) - taken_option.pack(side=LEFT, expand = True, fill = "both") - #type_label.pack(side=LEFT, expand = True, fill = None) - #type_option.pack(side=LEFT, expand = True, fill = "both") - - self.UpdateTakenTable() + type_checkbox_frame.grid( + row=1, column=0, columnspan=7, sticky="nsew", pady=5) + checkbox_frame.grid(row=2, column=0, columnspan=7, sticky="nsew") + copy_button.grid(row=3, column=0, columnspan=7, sticky="nsew") + taken_table_frame.grid( + row=4, column=0, columnspan=7, sticky="nsew") + + self.taken_table.pack(side=tkinter.LEFT, expand=True, fill="both") + + taken_creature_checkbox.pack( + side=tkinter.LEFT, expand=True, fill="both") + + taken_land_checkbox.pack( + side=tkinter.LEFT, expand=True, fill="both") + + taken_instant_sorcery_checkbox.pack( + side=tkinter.LEFT, expand=True, fill="both") + + taken_other_checkbox.pack( + side=tkinter.LEFT, expand=True, fill="both") + + taken_alsa_checkbox.pack( + side=tkinter.LEFT, expand=True, fill="both") + taken_ata_checkbox.pack( + side=tkinter.LEFT, expand=True, fill="both") + taken_gpwr_checkbox.pack( + side=tkinter.LEFT, expand=True, fill="both") + taken_ohwr_checkbox.pack( + side=tkinter.LEFT, expand=True, fill="both") + taken_gdwr_checkbox.pack( + side=tkinter.LEFT, expand=True, fill="both") + taken_gndwr_checkbox.pack( + side=tkinter.LEFT, expand=True, fill="both") + taken_iwd_checkbox.pack( + side=tkinter.LEFT, expand=True, fill="both") + + taken_filter_label.pack(side=tkinter.LEFT, expand=True, fill=None) + taken_option.pack(side=tkinter.LEFT, expand=True, fill="both") + + self._update_taken_table() popup.update() except Exception as error: - overlay_logger.info(f"TakenCardsPopup Error: {error}") - - def SuggestDeckPopup(self): - popup = Toplevel() + overlay_logger.info("__open_taken_cards_window Error: %s", error) + self._control_trace(True) + + def _open_suggest_deck_window(self): + '''Creates the Suggest Deck window''' + popup = tkinter.Toplevel() popup.wm_title("Suggested Decks") popup.attributes("-topmost", True) - popup.resizable(width = False, height = True) - - x, y = SafeCoordinates(self.root, 400, 170, 250, 0) - popup.wm_geometry("+%d+%d" % (x, y)) - + popup.resizable(width=False, height=False) + + location_x, location_y = identify_safe_coordinates(self.root, + self._scale_value( + 400), + self._scale_value( + 170), + self._scale_value( + 250), + self._scale_value(0)) + popup.wm_geometry(f"+{location_x}+{location_y}") + try: - Grid.rowconfigure(popup, 3, weight = 1) - - suggested_decks = CL.SuggestDeck(self.draft.TakenCards(), self.set_metrics, self.configuration) - + tkinter.Grid.rowconfigure(popup, 3, weight=1) + + suggested_decks = CL.suggest_deck( + self.draft.retrieve_taken_cards(), self.set_metrics, self.configuration) + choices = ["None"] deck_color_options = {} - - if len(suggested_decks): + + if suggested_decks: choices = [] - for color in suggested_decks: - rating_label = "%s %s (Rating:%d)" % (color, suggested_decks[color]["type"], suggested_decks[color]["rating"]) - deck_color_options[rating_label] = color + for key, value in suggested_decks.items(): + rating_label = f"{key} {value['type']} (Rating:{value['rating']})" + deck_color_options[rating_label] = key choices.append(rating_label) - - deck_colors_label = Label(popup, text="Deck Colors:", anchor = 'e', font=f'{constants.FONT_SANS_SERIF} 9 bold') - - deck_colors_value = StringVar(popup) - deck_colors_entry = OptionMenu(popup, deck_colors_value, choices[0], *choices) - - deck_colors_button = Button(popup, command=lambda:self.UpdateSuggestDeckTable(suggest_table, + + deck_colors_label = Label( + popup, text="Deck Colors:", anchor='e', style="MainSections.TLabel") + + deck_colors_value = tkinter.StringVar(popup) + deck_colors_entry = OptionMenu( + popup, deck_colors_value, choices[0], *choices) + menu = self.root.nametowidget(deck_colors_entry['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) + + deck_colors_button = Button(popup, command=lambda: self._update_suggest_table(suggest_table, deck_colors_value, suggested_decks, deck_color_options), - text="Update") - - copy_button = Button(popup, command=lambda:CopySuggested(deck_colors_value, - suggested_decks, - self.draft.set_data, - deck_color_options), - text="Copy to Clipboard") - - headers = {"CARD" : {"width" : .35, "anchor" : W}, - "COUNT" : {"width" : .14, "anchor" : CENTER}, - "COLOR" : {"width" : .12, "anchor" : CENTER}, - "COST" : {"width" : .10, "anchor" : CENTER}, - "TYPE" : {"width" : .29, "anchor" : CENTER}} - - style = Style() - style.configure("Suggest.Treeview", rowheight=25) - - suggest_table_frame = Frame(popup) - suggest_scrollbar = Scrollbar(suggest_table_frame, orient=VERTICAL) - suggest_scrollbar.pack(side=RIGHT, fill=Y) - suggest_table = CreateHeader(suggest_table_frame, 0, 8, headers, 450, True, True, "Suggest.Treeview", False) + text="Update") + + copy_button = Button(popup, command=lambda: copy_suggested(deck_colors_value, + suggested_decks, + deck_color_options), + text="Copy to Clipboard") + + headers = {"CARD": {"width": .35, "anchor": tkinter.W}, + "COUNT": {"width": .14, "anchor": tkinter.CENTER}, + "COLOR": {"width": .12, "anchor": tkinter.CENTER}, + "COST": {"width": .10, "anchor": tkinter.CENTER}, + "TYPE": {"width": .29, "anchor": tkinter.CENTER}} + + #style = Style() + #style.configure("Suggest.Treeview", rowheight=self._scale_value(25)) + + suggest_table_frame = tkinter.Frame(popup) + suggest_scrollbar = tkinter.Scrollbar( + suggest_table_frame, orient=tkinter.VERTICAL) + suggest_scrollbar.pack(side=tkinter.RIGHT, fill=tkinter.Y) + suggest_table = create_header( + suggest_table_frame, 0, self.fonts_dict["All.TableRow"], headers, self._scale_value(450), True, True, "Suggest.Treeview", False) suggest_table.config(yscrollcommand=suggest_scrollbar.set) suggest_scrollbar.config(command=suggest_table.yview) - - deck_colors_label.grid(row=0,column=0,columnspan=1,sticky="nsew") - deck_colors_entry.grid(row=0,column=1,columnspan=1,sticky="nsew") - deck_colors_button.grid(row=1,column=0,columnspan=2,sticky="nsew") - copy_button.grid(row=2,column=0,columnspan=2,sticky="nsew") - suggest_table_frame.grid(row=3, column=0, columnspan = 2, sticky = 'nsew') - - suggest_table.pack(expand = True, fill = 'both') - - self.UpdateSuggestDeckTable(suggest_table, deck_colors_value, suggested_decks, deck_color_options) + + deck_colors_label.grid( + row=0, column=0, columnspan=1, sticky="nsew") + deck_colors_entry.grid( + row=0, column=1, columnspan=1, sticky="nsew") + deck_colors_button.grid( + row=1, column=0, columnspan=2, sticky="nsew") + copy_button.grid(row=2, column=0, columnspan=2, sticky="nsew") + suggest_table_frame.grid( + row=3, column=0, columnspan=2, sticky='nsew') + + suggest_table.pack(expand=True, fill='both') + + self._update_suggest_table( + suggest_table, deck_colors_value, suggested_decks, deck_color_options) except Exception as error: - overlay_logger.info(f"SuggestDeckPopup Error: {error}") + overlay_logger.info("__open_suggest_deck_window Error: %s", error) - def SettingsExit(self, popup): + def _close_settings_window(self, popup): + '''Clears settings data when the Settings window is closed''' self.column_2_options = None self.column_3_options = None self.column_4_options = None @@ -1451,226 +1964,302 @@ def SettingsExit(self, popup): self.column_6_options = None self.column_7_options = None popup.destroy() - - def SettingsPopup(self): - popup = Toplevel() + + def _open_settings_window(self): + '''Creates the Settings window''' + popup = tkinter.Toplevel() popup.wm_title("Settings") - popup.protocol("WM_DELETE_WINDOW", lambda window=popup: self.SettingsExit(window)) + popup.protocol("WM_DELETE_WINDOW", + lambda window=popup: self._close_settings_window(window)) popup.attributes("-topmost", True) - x, y = SafeCoordinates(self.root, 400, 170, 250, 0) - popup.wm_geometry("+%d+%d" % (x, y)) - + popup.resizable(width=False, height=False) + location_x, location_y = identify_safe_coordinates(self.root, + self._scale_value( + 400), + self._scale_value( + 170), + self._scale_value( + 250), + self._scale_value(0)) + popup.wm_geometry(f"+{location_x}+{location_y}") + try: - Grid.rowconfigure(popup, 1, weight = 1) - Grid.columnconfigure(popup, 0, weight = 1) - - self.ControlTrace(False) - - column_2_label = Label(popup, text="Column 2:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") - column_3_label = Label(popup, text="Column 3:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") - column_4_label = Label(popup, text="Column 4:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") - column_5_label = Label(popup, text="Column 5:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") - column_6_label = Label(popup, text="Column 6:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") - column_7_label = Label(popup, text="Column 7:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") - filter_format_label = Label(popup, text="Deck Filter Format:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") - result_format_label = Label(popup, text="Win Rate Format:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") - deck_stats_label = Label(popup, text="Enable Draft Stats:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") + #tkinter.Grid.rowconfigure(popup, 1, weight=1) + tkinter.Grid.columnconfigure(popup, 1, weight=1) + + self._control_trace(False) + + column_2_label = Label( + popup, text="Column 2:", style="MainSections.TLabel", anchor="w") + column_3_label = Label( + popup, text="Column 3:", style="MainSections.TLabel", anchor="w") + column_4_label = Label( + popup, text="Column 4:", style="MainSections.TLabel", anchor="w") + column_5_label = Label( + popup, text="Column 5:", style="MainSections.TLabel", anchor="w") + column_6_label = Label( + popup, text="Column 6:", style="MainSections.TLabel", anchor="w") + column_7_label = Label( + popup, text="Column 7:", style="MainSections.TLabel", anchor="w") + filter_format_label = Label( + popup, text="Deck Filter Format:", style="MainSections.TLabel", anchor="w") + result_format_label = Label( + popup, text="Win Rate Format:", style="MainSections.TLabel", anchor="w") + deck_stats_label = Label(popup, text="Enable Draft Stats:", + style="MainSections.TLabel", anchor="w") deck_stats_checkbox = Checkbutton(popup, variable=self.deck_stats_checkbox_value, onvalue=1, offvalue=0) - missing_cards_label = Label(popup, text="Enable Missing Cards:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") + missing_cards_label = Label( + popup, text="Enable Missing Cards:", style="MainSections.TLabel", anchor="w") missing_cards_checkbox = Checkbutton(popup, variable=self.missing_cards_checkbox_value, onvalue=1, offvalue=0) - - auto_highest_label = Label(popup, text="Enable Highest Rated:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") + + auto_highest_label = Label( + popup, text="Enable Highest Rated:", style="MainSections.TLabel", anchor="w") auto_highest_checkbox = Checkbutton(popup, - variable=self.auto_highest_checkbox_value, - onvalue=1, - offvalue=0) - - #curve_bonus_label = Label(popup, text="Enable Curve Bonus:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") - #curve_bonus_checkbox = Checkbutton(popup, - # variable=self.curve_bonus_checkbox_value, - # onvalue=1, - # offvalue=0) - #color_bonus_label = Label(popup, text="Enable Color Bonus:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") - #color_bonus_checkbox = Checkbutton(popup, - # variable=self.color_bonus_checkbox_value, - # onvalue=1, - # offvalue=0) - - bayesian_average_label = Label(popup, text="Enable Bayesian Average:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") + variable=self.auto_highest_checkbox_value, + onvalue=1, + offvalue=0) + + bayesian_average_label = Label( + popup, text="Enable Bayesian Average:", style="MainSections.TLabel", anchor="w") bayesian_average_checkbox = Checkbutton(popup, - variable=self.bayesian_average_checkbox_value, - onvalue=1, - offvalue=0) + variable=self.bayesian_average_checkbox_value, + onvalue=1, + offvalue=0) - draft_log_label = Label(popup, text="Enable Draft Log:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") + draft_log_label = Label(popup, text="Enable Draft Log:", + style="MainSections.TLabel", anchor="w") draft_log_checkbox = Checkbutton(popup, variable=self.draft_log_checkbox_value, onvalue=1, - offvalue=0) + offvalue=0) - card_colors_label = Label(popup, text="Enable Card Colors:", font=f'{constants.FONT_SANS_SERIF} 9 bold', anchor="w") + card_colors_label = Label( + popup, text="Enable Card Colors:", style="MainSections.TLabel", anchor="w") card_colors_checkbox = Checkbutton(popup, variable=self.card_colors_checkbox_value, onvalue=1, - offvalue=0) - - optionsStyle = Style() - optionsStyle.configure('my.TMenubutton', font=(constants.FONT_SANS_SERIF, 9)) - - self.column_2_options = OptionMenu(popup, self.column_2_selection, self.column_2_selection.get(), *self.column_2_list, style="my.TMenubutton") + offvalue=0) + + color_identity_label = Label( + popup, text="Enable Color Identity:", style="MainSections.TLabel", anchor="w") + color_identity_checkbox = Checkbutton(popup, + variable=self.color_identity_checkbox_value, + onvalue=1, + offvalue=0) + + self.column_2_options = OptionMenu(popup, self.column_2_selection, self.column_2_selection.get( + ), *self.column_2_list, style="All.TMenubutton") self.column_2_options.config(width=15) - - self.column_3_options = OptionMenu(popup, self.column_3_selection, self.column_3_selection.get(), *self.column_3_list, style="my.TMenubutton") + menu = self.root.nametowidget(self.column_2_options['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) + + self.column_3_options = OptionMenu(popup, self.column_3_selection, self.column_3_selection.get( + ), *self.column_3_list, style="All.TMenubutton") self.column_3_options.config(width=15) - - self.column_4_options = OptionMenu(popup, self.column_4_selection, self.column_4_selection.get(), *self.column_4_list, style="my.TMenubutton") + menu = self.root.nametowidget(self.column_3_options['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) + + self.column_4_options = OptionMenu(popup, self.column_4_selection, self.column_4_selection.get( + ), *self.column_4_list, style="All.TMenubutton") self.column_4_options.config(width=15) + menu = self.root.nametowidget(self.column_4_options['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) - self.column_5_options = OptionMenu(popup, self.column_5_selection, self.column_5_selection.get(), *self.column_5_list, style="my.TMenubutton") + self.column_5_options = OptionMenu(popup, self.column_5_selection, self.column_5_selection.get( + ), *self.column_5_list, style="All.TMenubutton") self.column_5_options.config(width=15) + menu = self.root.nametowidget(self.column_5_options['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) - self.column_6_options = OptionMenu(popup, self.column_6_selection, self.column_6_selection.get(), *self.column_6_list, style="my.TMenubutton") + self.column_6_options = OptionMenu(popup, self.column_6_selection, self.column_6_selection.get( + ), *self.column_6_list, style="All.TMenubutton") self.column_6_options.config(width=15) + menu = self.root.nametowidget(self.column_6_options['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) - self.column_7_options = OptionMenu(popup, self.column_7_selection, self.column_7_selection.get(), *self.column_7_list, style="my.TMenubutton") + self.column_7_options = OptionMenu(popup, self.column_7_selection, self.column_7_selection.get( + ), *self.column_7_list, style="All.TMenubutton") self.column_7_options.config(width=15) - - filter_format_options = OptionMenu(popup, self.filter_format_selection, self.filter_format_selection.get(), *self.filter_format_list, style="my.TMenubutton") + menu = self.root.nametowidget(self.column_7_options['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) + + filter_format_options = OptionMenu(popup, self.filter_format_selection, self.filter_format_selection.get( + ), *self.filter_format_list, style="All.TMenubutton") filter_format_options.config(width=15) - - result_format_options = OptionMenu(popup, self.result_format_selection, self.result_format_selection.get(), *self.result_format_list, style="my.TMenubutton") + menu = self.root.nametowidget(filter_format_options['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) + + result_format_options = OptionMenu(popup, self.result_format_selection, self.result_format_selection.get( + ), *self.result_format_list, style="All.TMenubutton") result_format_options.config(width=15) - - default_button = Button(popup, command=self.DefaultSettingsCallback, text="Default Settings") - - column_2_label.grid(row=0, column=0, columnspan=1, sticky="nsew", padx=(10,)) - column_3_label.grid(row=1, column=0, columnspan=1, sticky="nsew", padx=(10,)) - column_4_label.grid(row=2, column=0, columnspan=1, sticky="nsew", padx=(10,)) - column_5_label.grid(row=3, column=0, columnspan=1, sticky="nsew", padx=(10,)) - column_6_label.grid(row=4, column=0, columnspan=1, sticky="nsew", padx=(10,)) - column_7_label.grid(row=5, column=0, columnspan=1, sticky="nsew", padx=(10,)) - filter_format_label.grid(row=6, column=0, columnspan=1, sticky="nsew", padx=(10,)) - result_format_label.grid(row=7, column=0, columnspan=1, sticky="nsew", padx=(10,)) - self.column_2_options.grid(row=0, column=1, columnspan=1, sticky="nsew") - self.column_3_options.grid(row=1, column=1, columnspan=1, sticky="nsew") - self.column_4_options.grid(row=2, column=1, columnspan=1, sticky="nsew") - self.column_5_options.grid(row=3, column=1, columnspan=1, sticky="nsew") - self.column_6_options.grid(row=4, column=1, columnspan=1, sticky="nsew") - self.column_7_options.grid(row=5, column=1, columnspan=1, sticky="nsew") - filter_format_options.grid(row=6, column=1, columnspan=1, sticky="nsew") - result_format_options.grid(row=7, column=1, columnspan=1, sticky="nsew") - card_colors_label.grid(row=8, column=0, columnspan=1, sticky="nsew", padx=(10,)) - card_colors_checkbox.grid(row=8, column=1, columnspan=1, sticky="nsew", padx=(5,)) - deck_stats_label.grid(row=9, column=0, columnspan=1, sticky="nsew", padx=(10,)) - deck_stats_checkbox.grid(row=9, column=1, columnspan=1, sticky="nsew", padx=(5,)) - missing_cards_label.grid(row=10, column=0, columnspan=1, sticky="nsew", padx=(10,)) - missing_cards_checkbox.grid(row=10, column=1, columnspan=1, sticky="nsew", padx=(5,)) - auto_highest_label.grid(row=11, column=0, columnspan=1, sticky="nsew", padx=(10,)) - auto_highest_checkbox.grid(row=11, column=1, columnspan=1, sticky="nsew", padx=(5,)) - #curve_bonus_label.grid(row=12, column=0, columnspan=1, sticky="nsew", padx=(10,)) - #curve_bonus_checkbox.grid(row=12, column=1, columnspan=1, sticky="nsew", padx=(5,)) - #color_bonus_label.grid(row=14, column=0, columnspan=1, sticky="nsew", padx=(10,)) - #color_bonus_checkbox.grid(row=14, column=1, columnspan=1, sticky="nsew", padx=(5,)) - bayesian_average_label.grid(row=15, column=0, columnspan=1, sticky="nsew", padx=(10,)) - bayesian_average_checkbox.grid(row=15, column=1, columnspan=1, sticky="nsew", padx=(5,)) - draft_log_label.grid(row=16, column=0, columnspan=1, sticky="nsew", padx=(10,)) - draft_log_checkbox.grid(row=16, column=1, columnspan=1, sticky="nsew", padx=(5,)) - default_button.grid(row=17, column=0, columnspan=2, sticky="nsew") - - self.ControlTrace(True) + menu = self.root.nametowidget(result_format_options['menu']) + menu.config(font=self.fonts_dict["All.TMenubutton"]) + + default_button = Button( + popup, command=self._default_settings_callback, text="Default Settings") + + column_2_label.grid(row=0, column=0, columnspan=1, + sticky="nsew", padx=(10,)) + column_3_label.grid(row=1, column=0, columnspan=1, + sticky="nsew", padx=(10,)) + column_4_label.grid(row=2, column=0, columnspan=1, + sticky="nsew", padx=(10,)) + column_5_label.grid(row=3, column=0, columnspan=1, + sticky="nsew", padx=(10,)) + column_6_label.grid(row=4, column=0, columnspan=1, + sticky="nsew", padx=(10,)) + column_7_label.grid(row=5, column=0, columnspan=1, + sticky="nsew", padx=(10,)) + filter_format_label.grid( + row=6, column=0, columnspan=1, sticky="nsew", padx=(10,)) + result_format_label.grid( + row=7, column=0, columnspan=1, sticky="nsew", padx=(10,)) + self.column_2_options.grid( + row=0, column=1, columnspan=1, sticky="nsew") + self.column_3_options.grid( + row=1, column=1, columnspan=1, sticky="nsew") + self.column_4_options.grid( + row=2, column=1, columnspan=1, sticky="nsew") + self.column_5_options.grid( + row=3, column=1, columnspan=1, sticky="nsew") + self.column_6_options.grid( + row=4, column=1, columnspan=1, sticky="nsew") + self.column_7_options.grid( + row=5, column=1, columnspan=1, sticky="nsew") + filter_format_options.grid( + row=6, column=1, columnspan=1, sticky="nsew") + result_format_options.grid( + row=7, column=1, columnspan=1, sticky="nsew") + card_colors_label.grid( + row=8, column=0, columnspan=1, sticky="nsew", padx=(10,)) + card_colors_checkbox.grid( + row=8, column=1, columnspan=1, sticky="nsew", padx=(5,)) + color_identity_label.grid( + row=9, column=0, columnspan=1, sticky="nsew", padx=(10,)) + color_identity_checkbox.grid( + row=9, column=1, columnspan=1, sticky="nsew", padx=(5,)) + deck_stats_label.grid( + row=10, column=0, columnspan=1, sticky="nsew", padx=(10,)) + deck_stats_checkbox.grid( + row=10, column=1, columnspan=1, sticky="nsew", padx=(5,)) + missing_cards_label.grid( + row=11, column=0, columnspan=1, sticky="nsew", padx=(10,)) + missing_cards_checkbox.grid( + row=11, column=1, columnspan=1, sticky="nsew", padx=(5,)) + auto_highest_label.grid( + row=12, column=0, columnspan=1, sticky="nsew", padx=(10,)) + auto_highest_checkbox.grid( + row=12, column=1, columnspan=1, sticky="nsew", padx=(5,)) + bayesian_average_label.grid( + row=13, column=0, columnspan=1, sticky="nsew", padx=(10,)) + bayesian_average_checkbox.grid( + row=13, column=1, columnspan=1, sticky="nsew", padx=(5,)) + draft_log_label.grid( + row=14, column=0, columnspan=1, sticky="nsew", padx=(10,)) + draft_log_checkbox.grid( + row=14, column=1, columnspan=1, sticky="nsew", padx=(5,)) + default_button.grid(row=15, column=0, columnspan=2, sticky="nsew") + + self._control_trace(True) except Exception as error: - overlay_logger.info(f"SettingsPopup Error: {error}") - - - def AddSet(self, popup, set, draft, start, end, button, progress, list_box, sets, status, version): + overlay_logger.info("__open_settings_window Error: %s", error) + + def _add_set(self, popup, draft_set, draft, start, end, button, progress, list_box, sets, status, version): + '''Initiates the set download process when the Add Set button is clicked''' result = True result_string = "" return_size = 0 - while(True): + while True: try: - message_box = MessageBox.askyesno(title="Download", message=f"17Lands updates their card data once a day at 01:30 UTC. Are you sure that you want to download {set.get()} {draft.get()} data?") + message_box = tkinter.messagebox.askyesno( + title="Download", message=f"17Lands updates their card data once a day at 03:00 UTC. Are you sure that you want to download {draft_set.get()} {draft.get()} data?") if not message_box: break - + status.set("Starting Download Process") - self.extractor.ClearData() + self.extractor.clear_data() button['state'] = 'disabled' progress['value'] = 0 popup.update() - self.extractor.Sets(sets[set.get()]) - self.extractor.DraftType(draft.get()) - if self.extractor.StartDate(start.get()) == False: + self.extractor.select_sets(sets[draft_set.get()]) + self.extractor.set_draft_type(draft.get()) + if not self.extractor.set_start_date(start.get()): result = False result_string = "Invalid Start Date (YYYY-MM-DD)" break - if self.extractor.EndDate(end.get()) == False: + if not self.extractor.set_end_date(end.get()): result = False result_string = "Invalid End Date (YYYY-MM-DD)" break - self.extractor.Version(version) + self.extractor.set_version(version) status.set("Downloading Color Ratings") - self.extractor.SessionColorRatings() - - result, result_string, temp_size = self.extractor.DownloadCardData(popup, progress, status, self.configuration.database_size) - - if result == False: - break - - if self.extractor.ExportData() == False: + self.extractor.retrieve_17lands_color_ratings() + + result, result_string, temp_size = self.extractor.download_card_data( + popup, progress, status, self.configuration.database_size) + + if not result: + break + + if not self.extractor.export_card_data(): result = False result_string = "File Write Failure" break - progress['value']=100 + progress['value'] = 100 button['state'] = 'normal' return_size = temp_size popup.update() status.set("Updating Set List") - self.DataViewUpdate(list_box, sets) - self.DraftReset(True) - self.UpdateCallback(True) + self._update_set_table(list_box, sets) + self._reset_draft(True) + self._update_overlay_callback(True) status.set("Download Complete") except Exception as error: result = False result_string = error - + break - - if result == False: + + if not result: status.set("Download Failed") popup.update() button['state'] = 'normal' - message_string = "Download Failed: %s" % result_string - message_box = MessageBox.showwarning(title="Error", message=message_string) + message_string = f"Download Failed: {result_string}" + message_box = tkinter.messagebox.showwarning( + title="Error", message=message_string) else: self.configuration.database_size = return_size - CL.WriteConfig(self.configuration) + CL.write_config(self.configuration) popup.update() return - - def DataViewUpdate(self, list_box, sets): - #Delete the content of the list box + + def _update_set_table(self, list_box, sets): + '''Updates the set list in the Set View table''' + # Delete the content of the list box for row in list_box.get_children(): - list_box.delete(row) + list_box.delete(row) self.root.update() - file_list = FE.RetrieveLocalSetList(sets) - - if len(file_list): - list_box.config(height = len(file_list)) + file_list = FE.retrieve_local_set_list(sets) + + if file_list: + list_box.config(height=len(file_list)) else: list_box.config(height=0) - + for count, file in enumerate(file_list): - row_tag = TableRowTag(False, "", count) - list_box.insert("",index = count, iid = count, values = file, tag = (row_tag,)) - - def OnClickTable(self, event, table, card_list, selected_color): + row_tag = identify_table_row_tag(False, "", count) + list_box.insert("", index=count, iid=count, + values=file, tag=(row_tag,)) + + def _process_table_click(self, event, table, card_list, selected_color): + '''Creates the card tooltip when a table row is clicked''' color_dict = {} for item in table.selection(): card_name = table.item(item, "value")[0] @@ -1678,278 +2267,354 @@ def OnClickTable(self, event, table, card_list, selected_color): card_name = card_name if card_name[0] != '*' else card_name[1:] if card_name == card[constants.DATA_FIELD_NAME]: try: - for count, color in enumerate(selected_color): - color_dict[color] = {x : "NA" for x in constants.DATA_FIELDS_LIST} - for k in color_dict[color].keys(): + for color in selected_color: + color_dict[color] = { + x: "NA" for x in constants.DATA_FIELDS_LIST} + for k in color_dict[color]: if k in card[constants.DATA_FIELD_DECK_COLORS][color]: - if k in constants.WIN_RATE_FIELDS_DICT.keys(): + if k in constants.WIN_RATE_FIELDS_DICT: winrate_count = constants.WIN_RATE_FIELDS_DICT[k] - color_dict[color][k] = CL.CalculateWinRate(card[constants.DATA_FIELD_DECK_COLORS][color][k], - card[constants.DATA_FIELD_DECK_COLORS][color][winrate_count], - self.configuration.bayesian_average_enabled) + color_dict[color][k] = CL.calculate_win_rate(card[constants.DATA_FIELD_DECK_COLORS][color][k], + card[constants.DATA_FIELD_DECK_COLORS][color][winrate_count], + self.configuration.bayesian_average_enabled) else: color_dict[color][k] = card[constants.DATA_FIELD_DECK_COLORS][color][k] - if "curve_bonus" in card.keys() and len(card["curve_bonus"]): - color_dict[color]["curve_bonus"] = card["curve_bonus"][count] - - if "color_bonus" in card.keys() and len(card["color_bonus"]): - color_dict[color]["color_bonus"] = card["color_bonus"][count] - - CreateCardToolTip(table, + CreateCardToolTip(table, event, card[constants.DATA_FIELD_NAME], color_dict, card[constants.DATA_SECTION_IMAGES], - self.configuration.images_enabled) + self.configuration.images_enabled, + self.scale_factor, + self.fonts_dict) except Exception as error: - overlay_logger.info(f"OnClickTable Error: {error}") + overlay_logger.info( + "__process_table_click Error: %s", error) break - def FileOpen(self): + + def _open_draft_log(self): + '''Reads and processes a stored draft log when File->Open is selected''' filename = filedialog.askopenfilename(filetypes=(("Log Files", "*.log"), - ("All files", "*.*") )) - + ("All files", "*.*"))) + if filename: self.arena_file = filename - self.DraftReset(True) - self.draft.ArenaFile(filename) - self.draft.LogSuspend(True) - self.UpdateCallback(True) - self.draft.LogSuspend(False) - - def ControlTrace(self, enabled): + self._reset_draft(True) + self.draft.set_arena_file(filename) + self.draft.log_suspend(True) + self._update_overlay_callback(True) + self.draft.log_suspend(False) + + def _control_trace(self, enabled): + '''Enable/Disable all of the overlay widget traces. This function is used when the application needs + to modify a widget value without triggering a callback + ''' try: trace_list = [ - (self.column_2_selection, lambda : self.column_2_selection.trace("w", self.UpdateSettingsCallback)), - (self.column_3_selection, lambda : self.column_3_selection.trace("w", self.UpdateSettingsCallback)), - (self.column_4_selection, lambda : self.column_4_selection.trace("w", self.UpdateSettingsCallback)), - (self.column_5_selection, lambda : self.column_5_selection.trace("w", self.UpdateSettingsCallback)), - (self.column_6_selection, lambda : self.column_6_selection.trace("w", self.UpdateSettingsCallback)), - (self.column_7_selection, lambda : self.column_7_selection.trace("w", self.UpdateSettingsCallback)), - (self.deck_stats_checkbox_value, lambda : self.deck_stats_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.missing_cards_checkbox_value, lambda : self.missing_cards_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.auto_highest_checkbox_value, lambda : self.auto_highest_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.curve_bonus_checkbox_value, lambda : self.curve_bonus_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.color_bonus_checkbox_value, lambda : self.color_bonus_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.bayesian_average_checkbox_value, lambda : self.bayesian_average_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.data_source_selection, lambda : self.data_source_selection.trace("w", self.UpdateSourceCallback)), - (self.stat_options_selection, lambda : self.stat_options_selection.trace("w", self.UpdateDeckStatsCallback)), - (self.draft_log_checkbox_value, lambda : self.draft_log_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.filter_format_selection, lambda : self.filter_format_selection.trace("w", self.UpdateSourceCallback)), - (self.result_format_selection, lambda : self.result_format_selection.trace("w", self.UpdateSourceCallback)), - (self.deck_filter_selection, lambda : self.deck_filter_selection.trace("w", self.UpdateSourceCallback)), - (self.taken_alsa_checkbox_value, lambda : self.taken_alsa_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.taken_ata_checkbox_value, lambda : self.taken_ata_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.taken_gpwr_checkbox_value, lambda : self.taken_gpwr_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.taken_ohwr_checkbox_value, lambda : self.taken_ohwr_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.taken_gndwr_checkbox_value, lambda : self.taken_gndwr_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.taken_iwd_checkbox_value, lambda : self.taken_iwd_checkbox_value.trace("w", self.UpdateSettingsCallback)), - (self.taken_filter_selection, lambda : self.taken_filter_selection.trace("w", self.UpdateSettingsCallback)), - (self.taken_type_selection, lambda : self.taken_type_selection.trace("w", self.UpdateSettingsCallback)), - (self.card_colors_checkbox_value, lambda : self.card_colors_checkbox_value.trace("w", self.UpdateSettingsCallback)), + (self.column_2_selection, lambda: self.column_2_selection.trace( + "w", self._update_settings_callback)), + (self.column_3_selection, lambda: self.column_3_selection.trace( + "w", self._update_settings_callback)), + (self.column_4_selection, lambda: self.column_4_selection.trace( + "w", self._update_settings_callback)), + (self.column_5_selection, lambda: self.column_5_selection.trace( + "w", self._update_settings_callback)), + (self.column_6_selection, lambda: self.column_6_selection.trace( + "w", self._update_settings_callback)), + (self.column_7_selection, lambda: self.column_7_selection.trace( + "w", self._update_settings_callback)), + (self.deck_stats_checkbox_value, lambda: self.deck_stats_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.missing_cards_checkbox_value, lambda: self.missing_cards_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.auto_highest_checkbox_value, lambda: self.auto_highest_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.curve_bonus_checkbox_value, lambda: self.curve_bonus_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.color_bonus_checkbox_value, lambda: self.color_bonus_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.bayesian_average_checkbox_value, lambda: self.bayesian_average_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.data_source_selection, lambda: self.data_source_selection.trace( + "w", self._update_source_callback)), + (self.stat_options_selection, lambda: self.stat_options_selection.trace( + "w", self._update_deck_stats_callback)), + (self.draft_log_checkbox_value, lambda: self.draft_log_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.filter_format_selection, lambda: self.filter_format_selection.trace( + "w", self._update_source_callback)), + (self.result_format_selection, lambda: self.result_format_selection.trace( + "w", self._update_source_callback)), + (self.deck_filter_selection, lambda: self.deck_filter_selection.trace( + "w", self._update_source_callback)), + (self.taken_alsa_checkbox_value, lambda: self.taken_alsa_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.taken_ata_checkbox_value, lambda: self.taken_ata_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.taken_gpwr_checkbox_value, lambda: self.taken_gpwr_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.taken_ohwr_checkbox_value, lambda: self.taken_ohwr_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.taken_gdwr_checkbox_value, lambda: self.taken_gdwr_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.taken_gndwr_checkbox_value, lambda: self.taken_gndwr_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.taken_iwd_checkbox_value, lambda: self.taken_iwd_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.taken_filter_selection, lambda: self.taken_filter_selection.trace( + "w", self._update_settings_callback)), + (self.taken_type_selection, lambda: self.taken_type_selection.trace( + "w", self._update_settings_callback)), + (self.card_colors_checkbox_value, lambda: self.card_colors_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.color_identity_checkbox_value, lambda: self.color_identity_checkbox_value.trace( + "w", self._update_settings_callback)), + (self.taken_type_creature_checkbox_value, lambda: self.taken_type_creature_checkbox_value.trace( + "w", self._update_taken_table)), + (self.taken_type_land_checkbox_value, lambda: self.taken_type_land_checkbox_value.trace( + "w", self._update_taken_table)), + (self.taken_type_instant_sorcery_checkbox_value, lambda: self.taken_type_instant_sorcery_checkbox_value.trace( + "w", self._update_taken_table)), + (self.taken_type_other_checkbox_value, lambda: self.taken_type_other_checkbox_value.trace( + "w", self._update_taken_table)), ] if enabled: - if len(self.trace_ids) == 0: - for (x,y) in trace_list: - self.trace_ids.append(y()) - elif len(self.trace_ids): - for count, (x,y) in enumerate(trace_list): - x.trace_vdelete("w", self.trace_ids[count]) + if not self.trace_ids: + for trace_tuple in trace_list: + self.trace_ids.append(trace_tuple[1]()) + elif self.trace_ids: + for count, trace_tuple in enumerate(trace_list): + trace_tuple[0].trace_vdelete("w", self.trace_ids[count]) self.trace_ids = [] except Exception as error: - overlay_logger.info(f"ControlTrace Error: {error}") + overlay_logger.info("__control_trace Error: %s", error) - def DraftReset(self, full_reset): - self.draft.ClearDraft(full_reset) - - def VersionCheck(self): - #Version Check + def _reset_draft(self, full_reset): + '''Clear all of the stored draft data (i.e., draft type, draft set, collected cards, etc.)''' + self.draft.clear_draft(full_reset) + + def _update_overlay_build(self): + '''Checks the version.txt file in Github to determine if a new version of the application is available''' + # Version Check update_flag = False if sys.platform == constants.PLATFORM_ID_WINDOWS: try: import win32api - new_version_found, new_version = CheckVersion(self.extractor, self.version) + new_version_found, new_version = check_version( + self.extractor, self.version) if new_version_found: - message_string = "Update client %.2f to version %.2f" % (self.version, new_version) - message_box = MessageBox.askyesno(title="Update", message=message_string) - if message_box == True: - self.extractor.SessionRepositoryDownload("setup.exe") - self.root.destroy() - win32api.ShellExecute(0, "open", "setup.exe", None, None, 10) - + message_string = f"Update client {self.version} to version {new_version}" + message_box = tkinter.messagebox.askyesno( + title="Update", message=message_string) + if message_box: + if self.extractor.retrieve_repository_file("setup.exe"): + self.root.destroy() + win32api.ShellExecute( + 0, "open", "setup.exe", None, None, 10) + else: + update_flag = True + else: update_flag = True else: update_flag = True - + except Exception as error: - print(error) + overlay_logger.info("__update_overlay_build Error: %s", error) update_flag = True else: update_flag = True if update_flag: - self.UpdateUI() - self.ControlTrace(True) + self._arena_log_check() + self._control_trace(True) - def EnableDeckStates(self, enable): + def _enable_deck_stats_table(self, enable): + '''Hide/Display the Deck Stats table based on the application settings''' try: if enable: - self.stat_frame.grid(row=10, column = 0, columnspan = 2, sticky = 'nsew') - self.stat_table.grid(row=11, column = 0, columnspan = 2, sticky = 'nsew') + self.stat_frame.grid( + row=10, column=0, columnspan=2, sticky='nsew') + self.stat_table.grid( + row=11, column=0, columnspan=2, sticky='nsew') else: self.stat_frame.grid_remove() self.stat_table.grid_remove() - except Exception as error: - self.stat_frame.grid(row=10, column = 0, columnspan = 2, sticky = 'nsew') - self.stat_table.grid(row=11, column = 0, columnspan = 2, sticky = 'nsew') - def EnableMissingCards(self, enable): + except Exception: + self.stat_frame.grid(row=10, column=0, columnspan=2, sticky='nsew') + self.stat_table.grid(row=11, column=0, columnspan=2, sticky='nsew') + + def _enable_missing_cards_table(self, enable): + '''Hide/Display the Missing Cards table based on the application settings''' try: if enable: - self.missing_frame.grid(row = 8, column = 0, columnspan = 2, sticky = 'nsew') - self.missing_table_frame.grid(row = 9, column = 0, columnspan = 2) + self.missing_frame.grid( + row=8, column=0, columnspan=2, sticky='nsew') + self.missing_table_frame.grid(row=9, column=0, columnspan=2) else: self.missing_frame.grid_remove() self.missing_table_frame.grid_remove() - except Exception as error: - self.missing_frame.grid(row = 8, column = 0, columnspan = 2, sticky = 'nsew') - self.missing_table_frame.grid(row = 9, column = 0, columnspan = 2) + except Exception: + self.missing_frame.grid( + row=8, column=0, columnspan=2, sticky='nsew') + self.missing_table_frame.grid(row=9, column=0, columnspan=2) + +class CreateCardToolTip(ScaledWindow): + '''Class that's used to create the card tooltip that appears when a table row is clicked''' -class CreateCardToolTip(object): - def __init__(self, widget, event, card_name, color_dict, image, images_enabled): - self.waittime = 1 #miliseconds - self.wraplength = 180 #pixels + def __init__(self, widget, event, card_name, color_dict, image, images_enabled, scale_factor, fonts_dict): + super().__init__() + self.scale_factor = scale_factor + self.fonts_dict = fonts_dict + self.waittime = 1 # miliseconds + self.wraplength = 180 # pixels self.widget = widget self.card_name = card_name self.color_dict = color_dict self.image = image self.images_enabled = images_enabled - self.widget.bind("", self.Leave) - self.widget.bind("", self.Leave) + self.widget.bind("", self._leave) + self.widget.bind("", self._leave) self.id = None self.tw = None self.event = event self.images = [] - self.Enter() - - def Enter(self, event=None): - self.Schedule() + self._enter() - def Leave(self, event=None): - self.Unschedule() - self.HideTip() + def _enter(self, event=None): + '''Initiate creation of the tooltip widget''' + self._schedule() - def Schedule(self): - self.Unschedule() - self.id = self.widget.after(self.waittime, self.ShowTip) + def _leave(self, event=None): + '''Remove tooltip when the user hovers over the tooltip or clicks elsewhere''' + self._unschedule() + self._hide_tooltip() - def Unschedule(self): - id = self.id + def _schedule(self): + '''Creates the tooltip window widget and stores the id''' + self._unschedule() + self.id = self.widget.after(self.waittime, self._display_tooltip) + + def _unschedule(self): + '''Clear the stored widget data when the closing the tooltip''' + widget_id = self.id self.id = None - if id: - self.widget.after_cancel(id) + if widget_id: + self.widget.after_cancel(widget_id) - def ShowTip(self, event=None): + def _display_tooltip(self, event=None): + '''Function that builds and populates the tooltip window ''' try: - tt_width = 400 + row_height = self._scale_value(23) + tt_width = self._scale_value(500) # creates a toplevel window - self.tw = Toplevel(self.widget) + self.tw = tkinter.Toplevel(self.widget) # Leaves only the label and removes the app window self.tw.wm_overrideredirect(True) if sys.platform == constants.PLATFORM_ID_OSX: - self.tw.wm_overrideredirect(False) - - tt_frame = Frame(self.tw, borderwidth=5,relief="solid") + self.tw.wm_overrideredirect(False) + + tt_frame = tkinter.Frame(self.tw, borderwidth=5, relief="solid") - Grid.rowconfigure(tt_frame, 2, weight = 1) + tkinter.Grid.rowconfigure(tt_frame, 2, weight=1) + + card_label = Label(tt_frame, + text=self.card_name, + style="TooltipHeader.TLabel", + background="#3d3d3d", + foreground="#e6ecec", + relief="groove", + anchor="c",) - card_label = Label(tt_frame, text=self.card_name, font=(constants.FONT_SANS_SERIF, 15, "bold", ), background = "#3d3d3d", foreground = "#e6ecec", relief="groove",anchor="c",) - if len(self.color_dict) == 2: - headers = {"Label" : {"width" : .70, "anchor" : W}, - "Value1" : {"width" : .15, "anchor" : CENTER}, - "Value2" : {"width" : .15, "anchor" : CENTER}} - width = 400 - tt_width += 150 + headers = {"Label": {"width": .70, "anchor": tkinter.W}, + "Value1": {"width": .15, "anchor": tkinter.CENTER}, + "Value2": {"width": .15, "anchor": tkinter.CENTER}} + width = self._scale_value(400) + tt_width += self._scale_value(125) else: - headers = {"Label" : {"width" : .80, "anchor" : W}, - "Value1" : {"width" : .20, "anchor" : CENTER}} - width = 340 - - style = Style() - style.configure("Tooltip.Treeview", rowheight=23) - - stats_main_table = CreateHeader(tt_frame, 0, 8, headers, width, False, True, "Tooltip.Treeview", False) + headers = {"Label": {"width": .80, "anchor": tkinter.W}, + "Value1": {"width": .20, "anchor": tkinter.CENTER}} + width = self._scale_value(340) + + style = Style() + style.configure("Tooltip.Treeview", rowheight=row_height) + + stats_main_table = create_header( + tt_frame, 0, self.fonts_dict["All.TableRow"], headers, width, False, True, "Tooltip.Treeview", False) main_field_list = [] - + values = ["Filter:"] + list(self.color_dict.keys()) main_field_list.append(tuple(values)) - values = ["Average Taken At:"] + [f"{x[constants.DATA_FIELD_ATA]}" for x in self.color_dict.values()] + values = ["Average Taken At:"] + \ + [f"{x[constants.DATA_FIELD_ATA]}" for x in self.color_dict.values()] main_field_list.append(tuple(values)) - - values = ["Average Last Seen At:"] + [f"{x[constants.DATA_FIELD_ALSA]}" for x in self.color_dict.values()] + + values = ["Average Last Seen At:"] + \ + [f"{x[constants.DATA_FIELD_ALSA]}" for x in self.color_dict.values()] main_field_list.append(tuple(values)) - - values = ["Improvement When Drawn:"] + [f"{x[constants.DATA_FIELD_IWD]}pp" for x in self.color_dict.values()] + + values = ["Improvement When Drawn:"] + \ + [f"{x[constants.DATA_FIELD_IWD]}pp" for x in self.color_dict.values()] main_field_list.append(tuple(values)) - - values = ["Games In Hand Win Rate:"] + [f"{x[constants.DATA_FIELD_GIHWR]}%" for x in self.color_dict.values()] + + values = ["Games In Hand Win Rate:"] + \ + [f"{x[constants.DATA_FIELD_GIHWR]}%" for x in self.color_dict.values()] main_field_list.append(tuple(values)) - - values = ["Opening Hand Win Rate:"] + [f"{x[constants.DATA_FIELD_OHWR]}%" for x in self.color_dict.values()] + + values = ["Opening Hand Win Rate:"] + \ + [f"{x[constants.DATA_FIELD_OHWR]}%" for x in self.color_dict.values()] main_field_list.append(tuple(values)) - values = ["Games Played Win Rate:"] + [f"{x[constants.DATA_FIELD_GPWR]}%" for x in self.color_dict.values()] + values = ["Games Played Win Rate:"] + \ + [f"{x[constants.DATA_FIELD_GPWR]}%" for x in self.color_dict.values()] main_field_list.append(tuple(values)) - values = ["Games Not Drawn Win Rate:"] + [f"{x[constants.DATA_FIELD_GNDWR]}%" for x in self.color_dict.values()] + values = ["Games Drawn Win Rate:"] + \ + [f"{x[constants.DATA_FIELD_GDWR]}%" for x in self.color_dict.values()] main_field_list.append(tuple(values)) - main_field_list.append(tuple(["",""])) - - values = ["Number of Games In Hand:"] + [f"{x[constants.DATA_FIELD_GIH]}" for x in self.color_dict.values()] + values = ["Games Not Drawn Win Rate:"] + \ + [f"{x[constants.DATA_FIELD_GNDWR]}%" for x in self.color_dict.values()] main_field_list.append(tuple(values)) - - values = ["Number of Games in Opening Hand:"] + [f"{x[constants.DATA_FIELD_NGOH]}" for x in self.color_dict.values()] + + main_field_list.append(tuple(["", ""])) + + values = ["Number of Games In Hand:"] + \ + [f"{x[constants.DATA_FIELD_GIH]}" for x in self.color_dict.values()] main_field_list.append(tuple(values)) - - values = ["Number of Games Played:"] + [f"{x[constants.DATA_FIELD_NGP]}" for x in self.color_dict.values()] + + values = ["Number of Games in Opening Hand:"] + \ + [f"{x[constants.DATA_FIELD_NGOH]}" for x in self.color_dict.values()] main_field_list.append(tuple(values)) - values = ["Number of Games Not Drawn:"] + [f"{x[constants.DATA_FIELD_NGND]}" for x in self.color_dict.values()] + values = ["Number of Games Played:"] + \ + [f"{x[constants.DATA_FIELD_NGP]}" for x in self.color_dict.values()] main_field_list.append(tuple(values)) - for x in range(4): - main_field_list.append(tuple(["",""])) - - #if any("curve_bonus" in x for x in self.color_dict.values()): - # values = ["Curve Bonus:"] + [f"+{x['curve_bonus']}" for x in self.color_dict.values()] - # main_field_list.append(tuple(values)) - #else: - # main_field_list.append(tuple(["",""])) - # - #if any("color_bonus" in x for x in self.color_dict.values()): - # values = ["Color Bonus:"] + [f"+{x['color_bonus']}" for x in self.color_dict.values()] - # main_field_list.append(tuple(values)) - #else: - # main_field_list.append(tuple(["",""])) - - #main_field_list.append(tuple(["",""])) - - if len(main_field_list): - stats_main_table.config(height = len(main_field_list)) - else: - stats_main_table.config(height=1) + values = ["Number of Games Drawn:"] + \ + [f"{x[constants.DATA_FIELD_NGD]}" for x in self.color_dict.values()] + main_field_list.append(tuple(values)) + + values = ["Number of Games Not Drawn:"] + \ + [f"{x[constants.DATA_FIELD_NGND]}" for x in self.color_dict.values()] + main_field_list.append(tuple(values)) + + for x in range(2): + main_field_list.append(tuple(["", ""])) + + stats_main_table.config(height=len(main_field_list)) column_offset = 0 - #Add scryfall image + # Add scryfall image if self.images_enabled: - from PIL import Image, ImageTk - size = 280, 390 + image_size_y = len(main_field_list) * row_height + size = self._scale_value(280), image_size_y self.images = [] for count, picture in enumerate(self.image): try: @@ -1959,33 +2624,44 @@ def ShowTip(self, event=None): im.thumbnail(size, Image.ANTIALIAS) image = ImageTk.PhotoImage(im) image_label = Label(tt_frame, image=image) - image_label.grid(column=count, row=1, columnspan=1, rowspan=3) + image_label.grid( + column=count, row=1, columnspan=1, rowspan=3) self.images.append(image) column_offset += 1 - tt_width += 300 + tt_width += self._scale_value(200) except Exception as error: - overlay_logger.info(f"ShowTip Image Error: {error}") - - card_label.grid(column=0, row=0, columnspan=column_offset + 2, sticky=NSEW) + overlay_logger.info( + "__display_tooltip Image Error: %s", error) - for count, row_values in enumerate(main_field_list): - row_tag = TableRowTag(False, "", count) - stats_main_table.insert("",index = count, iid = count, values = row_values, tag = (row_tag,)) + card_label.grid(column=0, row=0, + columnspan=column_offset + 2, sticky=tkinter.NSEW) + for count, row_values in enumerate(main_field_list): + row_tag = identify_table_row_tag(False, "", count) + stats_main_table.insert( + "", index=count, iid=count, values=row_values, tag=(row_tag,)) + + stats_main_table.grid( + row=1, column=column_offset, sticky=tkinter.NSEW) + + location_x, location_y = identify_safe_coordinates(self.tw, + self._scale_value( + tt_width), + self._scale_value( + 450), + self._scale_value( + 25), + self._scale_value(20)) + self.tw.wm_geometry(f"+{location_x}+{location_y}") - stats_main_table.grid(row = 1, column = column_offset, sticky=NSEW) - - x, y = SafeCoordinates(self.widget, tt_width, 500, 25, 20) - self.tw.wm_geometry("+%d+%d" % (x, y)) - tt_frame.pack() - + self.tw.attributes("-topmost", True) except Exception as error: - print("Showtip Error: %s" % error) + overlay_logger.info("__display_tooltip Error: %s", error) - def HideTip(self): + def _hide_tooltip(self): tw = self.tw - self.tw= None + self.tw = None if tw: - tw.destroy() \ No newline at end of file + tw.destroy() diff --git a/release_notes.txt b/release_notes.txt index 7a11291..fb1afda 100644 --- a/release_notes.txt +++ b/release_notes.txt @@ -1,3 +1,21 @@ +===================== RELEASE NOTES 3.03 ===================== +* Bug Fix: The application will use the UTF-8 codec when reading the Arena player.log file (Issue #17) + +* Bug Fix: Made some changes to address some text color and scaling issues (Issue #15) +** The dark mode theme is disabled for Mac users +** The config.json file has a scale_factor field that can be used to scale the application + +* Data Update: Added Games Drawn Win Rate and Number of Games Drawn data + +* Modified Feature: The application will now set the row colors based on a card's mana cost, ignoring kicker and ability costs (Issue #16) + +* Modified Feature: The Auto filter is now capable of identifying decks with 3-colors + +* New Feature: The new Enable Color Identity setting can be used to toggle a card's listed colors +** Enabling the setting will display a card's mana cost AND ability/kicker costs + +* New Feature: The Taken Cards window now has card-type filters (i.e., Creature, Lands, Instants/Sorceries, and Other) + ===================== RELEASE NOTES 3.02 ===================== * Data Update: Added support for the Arena Cube ** The set option is located at the bottom of the set list diff --git a/requirements.txt b/requirements.txt index 7e19486..402c079 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ Pillow>=9.0.0 pynput>=1.7.6 ttkwidgets>=0.12.1 pyinstaller>=4.8 -pywin32>=303 \ No newline at end of file +pywin32>=303 +numpy>=1.22.2 \ No newline at end of file diff --git a/requirements_mac.txt b/requirements_mac.txt index 1387cc5..150e014 100644 --- a/requirements_mac.txt +++ b/requirements_mac.txt @@ -1,3 +1,4 @@ Pillow>=9.0.0 pynput>=1.7.6 -ttkwidgets>=0.12.1 \ No newline at end of file +ttkwidgets>=0.12.1 +numpy>=1.22.2 \ No newline at end of file diff --git a/setup.exe b/setup.exe index 5e1507e..592f16b 100644 Binary files a/setup.exe and b/setup.exe differ