def single_card_text_internal(client: Client, requested_card: Card, disable_emoji: bool) -> str: mana = emoji.replace_emoji('|'.join(requested_card.mana_cost or []), client) mana = mana.replace('|', ' // ') legal = ' — ' + emoji.info_emoji(requested_card, verbose=True) if disable_emoji: legal = '' if requested_card.get('mode', None) == '$': text = '{name} {legal} — {price}'.format( name=requested_card.name, price=card_price.card_price_string(requested_card), legal=legal) else: text = '{name} {mana} — {type}{legal}'.format( name=requested_card.name, mana=mana, type=requested_card.type_line, legal=legal) if requested_card.bugs: for bug in requested_card.bugs: text += '\n:beetle:{rank} bug: {bug}'.format( bug=bug['description'], rank=bug['classification']) if bug['last_confirmed'] < (dtutil.now() - datetime.timedelta(days=60)): time_since_confirmed = (dtutil.now() - bug['last_confirmed']).total_seconds() text += ' (Last confirmed {time} ago.)'.format( time=dtutil.display_time(time_since_confirmed, 1)) return text
def read_rotation_files() -> Tuple[int, int, List[Card]]: runs_str = redis.get_str('decksite:rotation:summary:runs') runs_percent_str = redis.get_str('decksite:rotation:summary:runs_percent') cards = redis.get_list('decksite:rotation:summary:cards') if runs_str is not None and runs_percent_str is not None and cards is not None: return int(runs_str), int(runs_percent_str), [ Card(c, predetermined_values=True) for c in cards ] lines = [] fs = files() if len(fs) == 0: if not os.path.isdir(configuration.get_str('legality_dir')): raise DoesNotExistException( 'Invalid configuration. Could not find legality_dir.') return (0, 0, []) latest_list = open(fs[-1], 'r').read().splitlines() for filename in fs: for line in get_file_contents(filename): line = text.sanitize(line) lines.append(line.strip()) scores = Counter(lines).most_common() runs = scores[0][1] runs_percent = round(round(runs / TOTAL_RUNS, 2) * 100) cs = oracle.cards_by_name() cards = [] for name, hits in scores: c = process_score(name, hits, cs, runs, latest_list) if c is not None: cards.append(c) redis.store('decksite:rotation:summary:runs', runs, ex=604800) redis.store('decksite:rotation:summary:runs_percent', runs_percent, ex=604800) redis.store('decksite:rotation:summary:cards', cards, ex=604800) return (runs, runs_percent, cards)
def read_rotation_files() -> Tuple[int, int, List[Card]]: runs_str = redis.get_str('decksite:rotation:summary:runs') runs_percent_str = redis.get_str('decksite:rotation:summary:runs_percent') cards = redis.get_list('decksite:rotation:summary:cards') if runs_str is not None and runs_percent_str is not None and cards is not None: return int(runs_str), int(runs_percent_str), [Card(c, predetermined_values=True) for c in cards] return rotation_redis_store()
def if_todays_prices(out: bool = True) -> List[Card]: current_format = multiverse.get_format_id('Penny Dreadful') if out: not_clause = '' compare = '<' else: not_clause = 'NOT' compare = '>=' where = """ c.id {not_clause} IN (SELECT card_id FROM card_legality WHERE format_id = {format}) AND c.name in (SELECT name FROM `{prices_database}`.cache WHERE week {compare} 0.5) AND c.layout IN ({layouts}) """.format(not_clause=not_clause, format=current_format, prices_database=configuration.get('prices_database'), compare=compare, layouts=', '.join([ sqlescape(layout) for layout in multiverse.playable_layouts() ])) rs = db().select(multiverse.cached_base_query(where=where)) cards = [Card(r) for r in rs] return sorted(cards, key=lambda card: card['name'])
def load_cards(names: Iterable[str] = None, where: Optional[str] = None) -> List[Card]: if names == []: return [] if names: setnames = set(names) else: setnames = set() if setnames: names_clause = 'c.name IN ({names})'.format(names=', '.join( sqlescape(name) for name in setnames)) else: names_clause = '(1 = 1)' if where is None: where = '(1 = 1)' sql = multiverse.cached_base_query('({where} AND {names})'.format( where=where, names=names_clause)) rs = db().select(sql) if setnames and len(setnames) != len(rs): missing = setnames.symmetric_difference([r['name'] for r in rs]) raise TooFewItemsException( 'Expected `{namelen}` and got `{rslen}` with `{names}`. missing=`{missing}`' .format(namelen=len(setnames), rslen=len(rs), names=setnames, missing=missing)) return [Card(r) for r in rs]
async def download_scryfall_card_image(c: Card, filepath: str, version: str = '') -> bool: try: if c.is_double_sided(): paths = [ re.sub('.jpg$', '.a.jpg', filepath), re.sub('.jpg$', '.b.jpg', filepath) ] await fetch_tools.store_async(scryfall_image(c, version=version), paths[0]) if c.layout == 'transform' or c.layout == 'modal_dfc': await fetch_tools.store_async( scryfall_image(c, version=version, face='back'), paths[1]) if c.layout == 'meld': await fetch_tools.store_async( scryfall_image(c, version=version, face='meld'), paths[1]) if (fetch_tools.acceptable_file(paths[0]) and fetch_tools.acceptable_file(paths[1])): save_composite_image(paths, filepath) else: await fetch_tools.store_async(scryfall_image(c, version=version), filepath) except FetchException as e: print('Error: {e}'.format(e=e)) return fetch_tools.acceptable_file(filepath)
def search(query: str) -> List[Card]: where = parse(tokenize(query)) sql = """{base_query} ORDER BY pd_legal DESC, name """.format(base_query=multiverse.cached_base_query(where)) rs = db().select(sql) return [Card(r) for r in rs]
def deck_sort(c: Card) -> str: s = '' if c.is_creature(): s += 'A' elif c.is_land(): s += 'C' else: s += 'B' m = 'A' for cost in c.get('mana_cost') or (): if mana.has_x(cost): m = 'X' s += m s += str(c.cmc).zfill(10) s += c.name return s
def test_noimageavailable() -> None: c = Card({ 'name': "Barry's Land", 'id': 0, 'multiverseid': 0, 'names': "Barry's Land", 'layout': 'normal' }) assert image_fetcher.download_image([c]) is None
def query_diff_formats(f1: int, f2: int) -> Sequence[Card]: where = """ c.id IN (SELECT card_id FROM card_legality WHERE format_id = {format1}) AND c.id NOT IN (SELECT card_id FROM card_legality WHERE format_id = {format2}) """.format(format1=f1, format2=f2) rs = db().select(multiverse.cached_base_query(where=where)) out = [Card(r) for r in rs] return sorted(out, key=lambda card: card['name'])
async def add_cards_and_update_async(printings: List[CardDescription]) -> None: if not printings: return ids = await multiverse.insert_cards_async(printings) multiverse.add_to_cache(ids) cs = [ Card(r) for r in db().select( multiverse.cached_base_query('c.id IN (' + ','.join([str(id) for id in ids]) + ')')) ] whoosh_write.reindex_specific_cards(cs) for c in cs: CARDS_BY_NAME[c.name] = c
def scryfall_image(c: Card, version: str = '', face: str = None) -> str: if face == 'meld': name = c.names[1] elif ' // ' in c.name: name = c.name.replace(' // ', '/') else: name = c.name p = oracle.get_printing(c, c.get('preferred_printing')) if p is not None: u = f'https://api.scryfall.com/cards/{p.set_code}/{p.number}?format=image' else: u = 'https://api.scryfall.com/cards/named?exact={c}&format=image'.format(c=escape(name)) if version: u += '&version={v}'.format(v=escape(version)) if face and face != 'meld': u += '&face={f}'.format(f=escape(face)) return u
def test_hits_needed_score() -> None: assert rotation.hits_needed_score( Card({ 'name': 'Test', 'status': 'Undecided', 'hits': 33 })) == 135 assert rotation.hits_needed_score( Card({ 'name': 'Test', 'status': 'Undecided', 'hits': 1 })) == 167 assert rotation.hits_needed_score( Card({ 'name': 'Test', 'status': 'Legal', 'hits': 84 })) == 252 assert rotation.hits_needed_score( Card({ 'name': 'Test', 'status': 'Legal', 'hits': 130 })) == 298 assert rotation.hits_needed_score( Card({ 'name': 'Test', 'status': 'Legal', 'hits': 168 })) == 336 assert rotation.hits_needed_score( Card({ 'name': 'Test', 'status': 'Not Legal', 'hits': 83 })) == 421 assert rotation.hits_needed_score( Card({ 'name': 'Test', 'status': 'Not Legal', 'hits': 1 })) == 503
def prepare_card_urls(self, c: Card) -> None: c.url = self.url_for_card(c) c.img_url = self.url_for_image(c.name)
def prepare_card(self, c: Card) -> None: self.prepare_card_urls(c) c.card_img_class = 'two-faces' if c.layout in ['transform', 'meld' ] else '' c.pd_legal = c.legalities.get( 'Penny Dreadful', False) and c.legalities['Penny Dreadful'] != 'Banned' c.legal_formats = {k for k, v in c.legalities.items() if v != 'Banned'} c.non_pd_legal_formats = { k for k, v in c.legalities.items() if 'Penny Dreadful' not in k and v != 'Banned' } c.has_legal_format = len(c.legal_formats) > 0 prepare.set_legal_icons(c) if c.get('num_decks') is not None: c.show_record = c.get('wins') or c.get('losses') or c.get('draws') c.has_decks = len(c.get('decks', [])) > 0 if not c.has_decks: c.has_most_common_cards = False return counter = Counter() # type: ignore for d in c.get('decks', []): for c2 in d.maindeck: if not c2.card.type_line.startswith( 'Basic Land') and not c2['name'] == c.name: counter[c2['name']] += c2['n'] most_common_cards = counter.most_common(NUM_MOST_COMMON_CARDS_TO_LIST) c.most_common_cards = [] cs = oracle.cards_by_name() for v in most_common_cards: self.prepare_card(cs[v[0]]) c.most_common_cards.append(cs[v[0]]) c.has_most_common_cards = len(c.most_common_cards) > 0
def bugged_cards() -> List[Card]: sql = multiverse.cached_base_query('bugs IS NOT NULL') rs = db().select(sql) return [Card(r) for r in rs]
def prepare_card(self, c: Card) -> None: try: tournament_only = self.tournament_only #type: ignore # mypy complains we haven't declared tournament_only, but that's fine since we're # catching the error if it isn't defined by a subclass except AttributeError: tournament_only = False if tournament_only: c.url = url_for('.card_tournament', name=c.name) else: c.url = url_for('.card', name=c.name) c.img_url = url_for('image', c=c.name) c.card_img_class = 'two-faces' if c.layout in ['transform', 'meld'] else '' c.pd_legal = c.legalities.get('Penny Dreadful', False) and c.legalities['Penny Dreadful'] != 'Banned' c.legal_formats = {k for k, v in c.legalities.items() if v != 'Banned'} c.has_legal_format = len(c.legal_formats) > 0 if c.get('num_decks') is not None: c.show_record = c.get('wins') or c.get('losses') or c.get('draws') c.has_decks = len(c.get('decks', [])) > 0 counter = Counter() # type: ignore for d in c.get('decks', []): for c2 in d.maindeck: if not c2.card.type_line.startswith('Basic Land') and not c2['name'] == c.name: counter[c2['name']] += c2['n'] most_common_cards = counter.most_common(NUM_MOST_COMMON_CARDS_TO_LIST) c.most_common_cards = [] cs = oracle.cards_by_name() for v in most_common_cards: self.prepare_card(cs[v[0]]) c.most_common_cards.append(cs[v[0]]) c.has_most_common_cards = len(c.most_common_cards) > 0
def get_all_cards() -> List[Card]: rs = db().select(cached_base_query()) return [Card(r) for r in rs]
def prepare_card_urls(c: Card, tournament_only: bool = False) -> None: c.url = url_for_card(c, tournament_only) c.img_url = url_for_image(c.name)
def test_rotation_sort_func() -> None: # First, set up the test data we are going to use. An imaginary scenario 120 runs in to the rotation process. num_runs_so_far = 120 remaining_runs = rotation.TOTAL_RUNS - num_runs_so_far target = rotation.TOTAL_RUNS / 2 cs = [ Card({ 'name': 'Sylvan Library', 'hit_in_last_run': True, 'hits': 30 }), Card({ 'name': 'Black Lotus', 'hit_in_last_run': True, 'hits': 10 }), Card({ 'name': 'Murderous Cut', 'hit_in_last_run': False, 'hits': 90 }), Card({ 'name': 'Wrenn and Six', 'hit_in_last_run': True, 'hits': 90 }), Card({ 'name': 'Abandon Hope', 'hit_in_last_run': True, 'hits': 90 }), Card({ 'name': 'Channel', 'hit_in_last_run': False, 'hits': 20 }), Card({ 'name': 'Brain in a Jar', 'hit_in_last_run': True, 'hits': 60 }), Card({ 'name': 'Firebolt', 'hit_in_last_run': False, 'hits': 40 }), Card({ 'name': 'Charming Prince', 'hit_in_last_run': True, 'hits': 80 }), Card({ 'name': 'Life from the Loam', 'hit_in_last_run': True, 'hits': 50 }), Card({ 'name': 'Fury Charm', 'hit_in_last_run': False, 'hits': 70 }), Card({ 'name': 'Dark Confidant', 'hit_in_last_run': True, 'hits': 100 }), Card({ 'name': 'Colossal Dreadmaw', 'hit_in_last_run': True, 'hits': 1 }), ] for c in cs: c.hits_needed = max(target - c.hits, 0) c.percent_needed = str( round(round(c.hits_needed / remaining_runs, 2) * 100)) if c.hits_needed == 0: c.status = 'Legal' elif c.hits_needed < remaining_runs: c.status = 'Undecided' else: c.status = 'Not Legal' rotation.rotation_sort(cs, 'hitInLastRun', 'DESC') # Hit in last run and still possible to make it, but not yet confirmed, most hits first. assert cs[0].name == 'Charming Prince' assert cs[1].name == 'Brain in a Jar' assert cs[2].name == 'Life from the Loam' # Followed by hit in last run and confirmed, least hits first. assert cs[3].name == 'Abandon Hope' assert cs[4].name == 'Wrenn and Six' assert cs[5].name == 'Dark Confidant' # Hit in last run but eliminated, most hits first. assert cs[6].name == 'Sylvan Library' assert cs[7].name == 'Black Lotus' assert cs[8].name == 'Colossal Dreadmaw' # No hit in last run but still possible to make it, most hits first. assert cs[9].name == 'Fury Charm' assert cs[10].name == 'Firebolt' # No hit in last run but confirmed, least hits first assert cs[11].name == 'Murderous Cut' # No hit in last run, confirmed out, most hits first. assert cs[12].name == 'Channel' rotation.rotation_sort(cs, 'hits', 'DESC') assert cs[0].name == 'Dark Confidant' assert cs[1].name == 'Abandon Hope' assert cs[2].name == 'Murderous Cut' assert cs[3].name == 'Wrenn and Six' assert cs[4].name == 'Charming Prince' assert cs[5].name == 'Fury Charm' assert cs[6].name == 'Brain in a Jar' assert cs[7].name == 'Life from the Loam' assert cs[8].name == 'Firebolt' assert cs[9].name == 'Sylvan Library' assert cs[10].name == 'Channel' assert cs[11].name == 'Black Lotus' assert cs[12].name == 'Colossal Dreadmaw' rotation.rotation_sort(cs, 'hitsNeeded', 'ASC') # First we expect the cards that are nearly there, closest first. assert cs[0].name == 'Charming Prince' # 80 / 120 assert cs[1].name == 'Fury Charm' # 70 / 120 assert cs[2].name == 'Brain in a Jar' # … assert cs[3].name == 'Life from the Loam' assert cs[4].name == 'Firebolt' # Then cards that have made it, least hits first, to maximize the chance of showing cards that recently made it in near the top. assert cs[5].name == 'Abandon Hope' assert cs[6].name == 'Murderous Cut' assert cs[7].name == 'Wrenn and Six' assert cs[8].name == 'Dark Confidant' # Then cards that have been eliminated, most hits first. assert cs[9].name == 'Sylvan Library' assert cs[10].name == 'Channel' assert cs[11].name == 'Black Lotus' assert cs[12].name == 'Colossal Dreadmaw' rotation.rotation_sort(cs, 'name', 'ASC') assert cs[0].name == 'Abandon Hope' assert cs[1].name == 'Black Lotus' assert cs[2].name == 'Brain in a Jar' assert cs[3].name == 'Channel' assert cs[4].name == 'Charming Prince' assert cs[5].name == 'Colossal Dreadmaw' assert cs[6].name == 'Dark Confidant' assert cs[7].name == 'Firebolt' assert cs[8].name == 'Fury Charm' assert cs[9].name == 'Life from the Loam' assert cs[10].name == 'Murderous Cut' assert cs[11].name == 'Sylvan Library' assert cs[12].name == 'Wrenn and Six'