Пример #1
0
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)))
Пример #2
0
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
Пример #3
0
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
Пример #4
0
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
Пример #5
0
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
Пример #6
0
def set_legality(d: Deck) -> None:
    d.legal_formats = legality.legal_formats(d)
Пример #7
0
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
Пример #8
0
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
Пример #10
0
def prize(d: Deck) -> int:
    return prize_by_finish(d.get('finish') or sys.maxsize)
Пример #11
0
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)
Пример #12
0
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)
Пример #13
0
 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)
Пример #14
0
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
Пример #15
0
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