def normalize(d: Deck) -> str: try: name = d.original_name name = name.lower() name = replace_space_alternatives(name) name = remove_pd(name) name = remove_hashtags(name) name = remove_brackets(name) name = strip_leading_punctuation(name) name = remove_leading_deck(name) unabbreviated = expand_common_abbreviations(name) if unabbreviated != name or name in ABBREVIATIONS.values(): name = unabbreviated elif whitelisted(name): pass elif name and d.get('archetype_name') and name == d.get('archetype_name', '').lower(): pass else: name = add_colors_if_no_deckname(name, d.get('colors')) name = normalize_colors(name) name = add_archetype_if_just_colors(name, d.get('archetype_name')) name = remove_mono_if_not_first_word(name) name = remove_profanity(name) name = ucase_trailing_roman_numerals(name) name = titlecase.titlecase(name) return correct_case_of_color_names(name) except ValueError: raise InvalidDataException('Failed to normalize {d}'.format(d=repr(d)))
def import_deck(decklist): cards = defaultdict(lambda: [0, 0]) sideboard = False comment_pattern = re.compile(r'\s*#(.*)$') number_pattern = re.compile(r'^\s*(\d+).?\s+') for line in decklist.split('\n'): line = re.sub(r'\s+', ' ', line.lower().strip()) # Strip comments. match = re.search(comment_pattern, line) comment = None if match: comment = match.group(1).strip() line = re.sub(comment_pattern, '', line) # Detect switch to sideboard listing. if comment == 'sideboard': sideboard = True # Skip empty lines. if not line: continue # Determine the number of cards. match = re.search(number_pattern, line) if match: number = int(match.group(1)) line = re.sub(number_pattern, '', line) else: number = 1 # Figure out which card it is. try: card = Card.objects.get( Q(ascii_name__iexact=line) | Q(name__iexact=line)) except Card.DoesNotExist: logging.debug(u'Unknown card: %s', line) continue # Tally this card. cards[card][sideboard] += number deck = Deck(user=User.objects.all()[0], name='test-deck-{0}'.format( ''.join(random.choice(string.lowercase) for _ in range(5)))) deck.save() for card, counts in cards.items(): for sideboard, number in enumerate(counts): if number: deck_card = DeckCard(deck=deck, number=number, card=card, sideboard=sideboard) deck_card.save() return deck
def set_colors(d: Deck) -> None: deck_colors: Set[str] = set() deck_colored_symbols: List[str] = [] for c in [entry.card for entry in d.maindeck + d.sideboard]: for cost in c.get('mana_cost') or (): if c.layout == 'split': continue # They might only be using one half so ignore it. card_symbols = mana.parse(cost) card_colors = mana.colors(card_symbols) deck_colors.update(card_colors['required']) card_colored_symbols = mana.colored_symbols(card_symbols) deck_colored_symbols += card_colored_symbols['required'] d.colors = mana.order(deck_colors) d.colored_symbols = deck_colored_symbols
def setup_matches(should_load_decks: bool, show_active_deck_names: bool, decks_by_id: Dict[int, Deck], matches: Sequence[Container]) -> None: for m in matches: m.date = dtutil.ts2dt(m.date) m.competition_end_date = dtutil.ts2dt(m.competition_end_date) if g: # https://github.com/PennyDreadfulMTG/Penny-Dreadful-Tools/issues/8435 m.competition_url = url_for('competition', competition_id=m.competition_id) if Deck(m).is_in_current_run() and not show_active_deck_names: m.opponent_deck_name = '(Active League Run)' if should_load_decks and m.opponent_deck_id is not None and decks_by_id.get( m.opponent_deck_id): m.opponent_deck = decks_by_id[m.opponent_deck_id] elif should_load_decks: m.opponent_deck = None
def vivify(decklist: DecklistType) -> Deck: validated: DecklistType = {'maindeck': {}, 'sideboard': {}} invalid_names = set() for section in ['maindeck', 'sideboard']: for name, n in decklist.get(section, {}).items(): try: validated[section][oracle.valid_name(name)] = n except InvalidDataException: invalid_names.add(name) if invalid_names: raise InvalidDataException('Invalid cards: {invalid_names}'.format(invalid_names='; '.join(invalid_names))) d = Deck({'maindeck': [], 'sideboard': []}) for section in ['maindeck', 'sideboard']: for name, n in validated.get(section, {}).items(): d[section].append(CardRef(name, n)) return d
def set_legality(d: Deck) -> None: d.legal_formats = legality.legal_formats(d)
def load_decks_heavy(where: str = '1 = 1', having: str = '1 = 1', order_by: Optional[str] = None, limit: str = '', season_id: Optional[int] = None ) -> List[Deck]: if order_by is None: order_by = 'active_date DESC, d.finish IS NULL, d.finish' sql = """ SELECT d.id, d.name AS original_name, d.created_date, d.updated_date, SUM(CASE WHEN dm.games > IFNULL(odm.games, 0) THEN 1 ELSE 0 END) AS wins, SUM(CASE WHEN dm.games < odm.games THEN 1 ELSE 0 END) AS losses, SUM(CASE WHEN dm.games = odm.games THEN 1 ELSE 0 END) AS draws, d.finish, d.archetype_id, d.url AS source_url, d.competition_id, c.name AS competition_name, c.end_date AS competition_end_date, c.top_n AS competition_top_n, ct.name AS competition_type_name, d.identifier, {person_query} AS person, p.id AS person_id, p.banned, p.discord_id, d.decklist_hash, d.retired, d.reviewed, s.name AS source_name, IFNULL(a.name, '') AS archetype_name, cache.normalized_name AS name, cache.colors, cache.colored_symbols, cache.legal_formats, ROUND(cache.omw * 100, 2) AS omw, season.id AS season_id, IFNULL(MAX(m.date), d.created_date) AS active_date FROM deck AS d LEFT JOIN person AS p ON d.person_id = p.id LEFT JOIN source AS s ON d.source_id = s.id LEFT JOIN archetype AS a ON d.archetype_id = a.id {competition_join} LEFT JOIN deck_cache AS cache ON d.id = cache.deck_id LEFT JOIN deck_match AS dm ON d.id = dm.deck_id LEFT JOIN `match` AS m ON dm.match_id = m.id LEFT JOIN deck_match AS odm ON odm.deck_id <> d.id AND dm.match_id = odm.match_id {season_join} WHERE ({where}) AND ({season_query}) GROUP BY d.id, d.competition_id, -- Every deck has only one competition_id but if we want to use competition_id in the HAVING clause we need this. season.id -- In theory this is not necessary as all decks are in a single season and we join on the date but MySQL cannot work that out so give it the hint it needs. HAVING {having} ORDER BY {order_by} {limit} """.format(person_query=query.person_query(), competition_join=query.competition_join(), season_join=query.season_join(), where=where, season_query=query.season_query(season_id, 'season.id'), having=having, order_by=order_by, limit=limit) db().execute('SET group_concat_max_len=100000') rows = db().select(sql) decks = [] for row in rows: d = Deck(row) d.maindeck = [] d.sideboard = [] d.competition_top_n = Top(d.competition_top_n or 0) d.colored_symbols = json.loads(d.colored_symbols or '[]') d.colors = json.loads(d.colors or '[]') d.legal_formats = set(json.loads(d.legal_formats or '[]')) d.active_date = dtutil.ts2dt(d.active_date) d.created_date = dtutil.ts2dt(d.created_date) d.updated_date = dtutil.ts2dt(d.updated_date) if d.competition_end_date: d.competition_end_date = dtutil.ts2dt(d.competition_end_date) d.can_draw = 'Divine Intervention' in [card.name for card in d.all_cards()] d.wins = int(d.wins) d.losses = int(d.losses) d.draws = int(d.draws) decks.append(d) load_cards(decks) load_competitive_stats(decks) for d in decks: expiry = 60 if d.is_in_current_run() else 3600 redis.store('decksite:deck:{id}'.format(id=d.id), d, ex=expiry) return decks
def deserialize_deck(sdeck: Container) -> Deck: deck = Deck(sdeck) deck.active_date = dtutil.ts2dt(deck.active_date) deck.created_date = dtutil.ts2dt(deck.created_date) deck.updated_date = dtutil.ts2dt(deck.updated_date) if deck.competition_end_date is not None: deck.competition_end_date = dtutil.ts2dt(deck.competition_end_date) deck.wins = int(deck.wins) deck.losses = int(deck.losses) deck.draws = int(deck.draws) if deck.get('omw') is not None: deck.omw = float(deck.omw) deck.maindeck = [CardRef(ref['name'], ref['n']) for ref in deck.maindeck] deck.sideboard = [CardRef(ref['name'], ref['n']) for ref in deck.sideboard] return deck
def test_legal_formats() -> None: d = Deck({'id': 0}) d.maindeck = [CardRef('Swamp', 59)] d.sideboard = [] assert len(d.all_cards()) == 59 formats = legality.legal_formats(d) assert len(formats) == 0 d.maindeck = [CardRef('Swamp', 60)] formats = legality.legal_formats(d) assert 'Penny Dreadful' in formats assert 'Legacy' in formats assert 'Penny Dreadful EMN' in formats formats = legality.legal_formats(d, {'Penny Dreadful'}) assert len(formats) == 1 assert 'Penny Dreadful' in formats assert 'Legacy' not in formats d.maindeck = [CardRef('Swamp', 55), CardRef('Think Twice', 5)] formats = legality.legal_formats(d) assert len(d.all_cards()) == 60 assert len(legality.legal_formats(d)) == 0 d.maindeck = [CardRef('Swamp', 56), CardRef('Think Twice', 4)] formats = legality.legal_formats(d) assert 'Legacy' in formats assert 'Modern' in formats d.sideboard = [CardRef('Swamp', 15), CardRef('Think Twice', 1)] formats = legality.legal_formats(d) assert len(legality.legal_formats(d)) == 0 d.maindeck = [CardRef('Swamp', 56), CardRef('Fork', 4)] d.sideboard = [CardRef('Swamp', 15)] formats = legality.legal_formats(d) assert 'Legacy' in formats assert 'Modern' not in formats d.maindeck = [CardRef('Swamp', 60)] d.sideboard = [CardRef('Swamp', 15)] formats = legality.legal_formats(d) assert 'Standard' in formats assert 'Modern' in formats assert 'Legacy' in formats assert 'Vintage' in formats assert 'Penny Dreadful' in formats assert 'Penny Dreadful EMN' in formats
def prize(d: Deck) -> int: return prize_by_finish(d.get('finish') or sys.maxsize)
def set_stars_and_top8(d: Deck) -> None: if d.finish == 1 and d.competition_top_n >= 1: d.top8_safe = '<span title="Winner">①</span>' d.stars_safe = '★★★' elif d.finish == 2 and d.competition_top_n >= 2: d.top8_safe = '<span title="Losing Finalist">②</span>' d.stars_safe = '★★' elif d.finish == 3 and d.competition_top_n >= 3: d.top8_safe = '<span title="Losing Semifinalist">④</span>' d.stars_safe = '★★' elif d.finish == 5 and d.competition_top_n >= 5: d.top8_safe = '<span title="Losing Quarterfinalist">⑧</span>' d.stars_safe = '★' else: d.top8_safe = '' if d.get('wins') is not None and d.get('losses') is not None: if d.wins - 5 >= d.losses: d.stars_safe = '★★' elif d.wins - 3 >= d.losses: d.stars_safe = '★' else: d.stars_safe = '' else: d.stars_safe = '' if len(d.stars_safe) > 0: d.stars_safe = '<span class="stars" title="Success Rating">{stars}</span>'.format( stars=d.stars_safe)
def prepare_deck(d: Deck) -> None: set_stars_and_top8(d) if d.get('colors') is not None: d.colors_safe = colors_html(d.colors, d.colored_symbols) if d.get('mtgo_username'): d.person_url = f'/people/{d.mtgo_username}/' else: d.person_url = f'/people/id/{d.person_id}/' d.date_sort = dtutil.dt2ts(d.active_date) d.display_date = dtutil.display_date(d.active_date) d.show_record = d.wins or d.losses or d.draws if d.competition_id: d.competition_url = '/competitions/{id}/'.format(id=d.competition_id) d.url = '/decks/{id}/'.format(id=d.id) d.export_url = '/export/{id}/'.format(id=d.id) d.cmc_chart_url = '/charts/cmc/{id}-cmc.png'.format(id=d.id) if d.is_in_current_run(): d.active_safe = '<span class="active" title="Active in the current league">⊕</span>' d.stars_safe = '{active} {stars}'.format(active=d.active_safe, stars=d.stars_safe).strip() d.source_sort = '1' d.source_is_external = not d.source_name == 'League' d.comp_row_len = len('{comp_name} (Piloted by {person}'.format( comp_name=d.competition_name, person=d.person)) if d.get('archetype_id', None): d.archetype_url = '/archetypes/{id}/'.format(id=d.archetype_id) # We might be getting '43%'/'' from cache or '43'/None from the db. Cope with all possibilities. # It might be better to use display_omw and omw as separate properties rather than overwriting the numeric value. if d.get('omw') is None or d.omw == '': d.omw = '' elif '%' not in str(d.omw): d.omw = str(int(d.omw)) + '%' d.has_legal_format = len(d.legal_formats) > 0 d.pd_legal = 'Penny Dreadful' in d.legal_formats d.non_pd_legal_formats = { f for f in d.legal_formats if 'Penny Dreadful' not in f } set_legal_icons(d) if session.get('admin') or session.get( 'demimod') or not d.is_in_current_run(): d.decklist = str(d) else: d.decklist = '' total, num_cards = 0, 0 for c in d.maindeck: if c.card.cmc is None: c.card.cmc = 0 if 'Land' not in c.card.type_line: num_cards += c['n'] total += c['n'] * c.card.cmc d.average_cmc = round(total / max(1, num_cards), 2)
def prepare_deck(self, d: Deck) -> None: set_stars_and_top8(d) if d.get('colors') is not None: d.colors_safe = colors_html(d.colors, d.colored_symbols) d.person_url = '/people/{id}/'.format(id=d.person_id) d.date_sort = dtutil.dt2ts(d.active_date) d.display_date = dtutil.display_date(d.active_date) d.show_record = d.wins or d.losses or d.draws if d.competition_id: d.competition_url = '/competitions/{id}/'.format(id=d.competition_id) d.url = '/decks/{id}/'.format(id=d.id) d.export_url = '/export/{id}/'.format(id=d.id) d.cmc_chart_url = '/charts/cmc/{id}-cmc.png'.format(id=d.id) if d.is_in_current_run(): d.active_safe = '<span class="active" title="Active in the current league">⊕</span>' d.stars_safe = '{active} {stars}'.format(active=d.active_safe, stars=d.stars_safe).strip() d.source_sort = '1' d.source_is_external = not d.source_name == 'League' d.comp_row_len = len('{comp_name} (Piloted by {person}'.format(comp_name=d.competition_name, person=d.person)) if d.get('archetype_id', None): d.archetype_url = '/archetypes/{id}/'.format(id=d.archetype_id) # We might be getting '43%'/'' from cache or '43'/None from the db. Cope with all possibilities. # It might be better to use display_omw and omw as separate properties rather than overwriting the numeric value. if d.get('omw') is None or d.omw == '': d.omw = '' elif '%' not in str(d.omw): d.omw = str(int(d.omw)) + '%' d.has_legal_format = len(d.legal_formats) > 0 d.pd_legal = 'Penny Dreadful' in d.legal_formats d.legal_icons = '' sets = rotation.SEASONS if 'Penny Dreadful' in d.legal_formats: icon = rotation.current_season_code().lower() n = sets.index(icon.upper()) + 1 d.legal_icons += '<a href="{url}"><i class="ss ss-{code} ss-rare ss-grad">S{n}</i></a>'.format(url='/seasons/{id}/'.format(id=n), code=icon, n=n) past_pd_formats = [fmt.replace('Penny Dreadful ', '') for fmt in d.legal_formats if 'Penny Dreadful ' in fmt] past_pd_formats.sort(key=lambda code: -sets.index(code)) for code in past_pd_formats: n = sets.index(code.upper()) + 1 d.legal_icons += '<a href="{url}"><i class="ss ss-{set} ss-common ss-grad">S{n}</i></a>'.format(url='/seasons/{id}/'.format(id=n), set=code.lower(), n=n) if 'Commander' in d.legal_formats: # I think C16 looks the nicest. d.legal_icons += '<i class="ss ss-c16 ss-uncommon ss-grad">CMDR</i>' if session.get('admin') or session.get('demimod') or not d.is_in_current_run(): d.decklist = str(d).replace('\n', '<br>') else: d.decklist = '' total, num_cards = 0, 0 for c in d.maindeck: if c.card.cmc is None: c.card.cmc = 0 if 'Land' not in c.card.type_line: num_cards += c['n'] total += c['n'] * c.card.cmc d.average_cmc = round(total / max(1, num_cards), 2)
def load_matches(where: str = 'TRUE', season_id: Optional[int] = None, should_load_decks: bool = False) -> List[Container]: person_query = query.person_query(table='o') competition_join = query.competition_join() season_join = query.season_join() season_query = query.season_query(season_id, 'season.id') sql = f""" SELECT m.`date`, m.id, m.`round`, m.elimination, m.mtgo_id, d.id AS deck_id, dc.normalized_name AS deck_name, od.id AS opponent_deck_id, odc.normalized_name AS opponent_deck_name, dm.games AS game_wins, IFNULL(odm.games, 0) AS game_losses, c.id AS competition_id, ct.name AS competition_type_name, c.end_date AS competition_end_date, {person_query} AS opponent, odc.wins, odc.draws, odc.losses, od.retired FROM `match` AS m INNER JOIN deck_match AS dm ON m.id = dm.match_id INNER JOIN deck AS d ON dm.deck_id = d.id INNER JOIN deck_cache AS dc ON d.id = dc.deck_id LEFT JOIN deck_match AS odm ON dm.match_id = odm.match_id AND odm.deck_id <> d.id LEFT JOIN deck AS od ON odm.deck_id = od.id LEFT JOIN deck_cache AS odc ON od.id = odc.deck_id LEFT JOIN person AS o ON od.person_id = o.id {competition_join} {season_join} WHERE {where} AND {season_query} ORDER BY m.`date`, m.`round` """ matches = [Container(r) for r in db().select(sql)] if should_load_decks: opponents = [ m.opponent_deck_id for m in matches if m.opponent_deck_id is not None ] if len(opponents) > 0: decks = deck.load_decks('d.id IN ({ids})'.format(ids=', '.join( [sqlescape(str(deck_id)) for deck_id in opponents]))) else: decks = [] decks_by_id = {d.id: d for d in decks} for m in matches: m.date = dtutil.ts2dt(m.date) m.competition_end_date = dtutil.ts2dt(m.competition_end_date) m.competition_url = url_for('competition', competition_id=m.competition_id) if Deck(m).is_in_current_run(): m.opponent_deck_name = '(Active League Run)' if should_load_decks and m.opponent_deck_id is not None and decks_by_id.get( m.opponent_deck_id): m.opponent_deck = decks_by_id[m.opponent_deck_id] elif should_load_decks: m.opponent_deck = None return matches
def load_matches(where: str = 'TRUE', order_by: str = 'm.`date`, m.`round`', limit: str = '', season_id: Union[int, str, None] = None, should_load_decks: bool = False, show_active_deck_names: bool = False) -> List[Container]: person_query = query.person_query() opponent_person_query = query.person_query(table='o') competition_join = query.competition_join() season_join = query.season_join() season_query = query.season_query(season_id, 'season.id') sql = f""" SELECT m.`date`, m.id, m.`round`, m.elimination, m.mtgo_id, d.id AS deck_id, {person_query} AS person, dc.normalized_name AS deck_name, od.id AS opponent_deck_id, odc.normalized_name AS opponent_deck_name, dm.games AS game_wins, IFNULL(odm.games, 0) AS game_losses, c.id AS competition_id, ct.name AS competition_type_name, c.end_date AS competition_end_date, {opponent_person_query} AS opponent, odc.wins, odc.draws, odc.losses, od.retired FROM `match` AS m INNER JOIN deck_match AS dm ON m.id = dm.match_id INNER JOIN deck AS d ON dm.deck_id = d.id INNER JOIN deck_cache AS dc ON d.id = dc.deck_id INNER JOIN person AS p ON d.person_id = p.id LEFT JOIN deck_match AS odm ON dm.match_id = odm.match_id AND odm.deck_id <> d.id LEFT JOIN deck AS od ON odm.deck_id = od.id LEFT JOIN deck_cache AS odc ON od.id = odc.deck_id LEFT JOIN person AS o ON od.person_id = o.id {competition_join} {season_join} WHERE {where} AND {season_query} GROUP BY m.id -- We don't want an row for each deck in a match (when the WHERE doesn't include a person) ORDER BY {order_by} {limit} """ matches = [Container(r) for r in db().select(sql)] if should_load_decks: opponents = [ m.opponent_deck_id for m in matches if m.opponent_deck_id is not None ] if len(opponents) > 0: decks = deck.load_decks('d.id IN ({ids})'.format(ids=', '.join( [sqlescape(str(deck_id)) for deck_id in opponents]))) else: decks = [] decks_by_id = {d.id: d for d in decks} for m in matches: m.date = dtutil.ts2dt(m.date) m.competition_end_date = dtutil.ts2dt(m.competition_end_date) if g: # https://github.com/PennyDreadfulMTG/Penny-Dreadful-Tools/issues/8435 m.competition_url = url_for('competition', competition_id=m.competition_id) if Deck(m).is_in_current_run() and not show_active_deck_names: m.opponent_deck_name = '(Active League Run)' if should_load_decks and m.opponent_deck_id is not None and decks_by_id.get( m.opponent_deck_id): m.opponent_deck = decks_by_id[m.opponent_deck_id] elif should_load_decks: m.opponent_deck = None return matches