diff --git a/analyze_cube_keywords.py b/analyze_cube_keywords.py index 55d134c..45e87eb 100644 --- a/analyze_cube_keywords.py +++ b/analyze_cube_keywords.py @@ -2,10 +2,11 @@ """ import cube_json +import cubecobra_csv import keyword_stats if __name__ == '__main__': - cube_list = cube_json.load_cube_from_csv('cube_csvs/TheElegantCube_2020-11-17_5.0.4.csv', {'core'}) + cube_list = cubecobra_csv.load_cube_names('cube_csvs/TheElegantCube_2020-11-17_5.0.4.csv', {'core'}) cube_json_filename = cube_json.create_cube_json(cube_list, 'cubes/TheElegantCube_2020-11-17_5.0.4.json') keyword_dict = keyword_stats.keyword_count(cube_json_filename) diff --git a/core_alphabetical.py b/core_alphabetical.py index 1f7657c..002caa0 100644 --- a/core_alphabetical.py +++ b/core_alphabetical.py @@ -2,26 +2,14 @@ import csv -import cube_json import cubecobra_csv CUBE_NAME = 'TheElegantCube_fetched' def main(): - csv_path = cube_json.request_cube_csv('TheElegantCube_fetched', 'elegant') - - core_cards = [] - with open(csv_path) as f: - reader = csv.reader(f) - header_line = next(reader) - cubecobra_csv.assert_header(header_line) - for line in reader: - card = line[cubecobra_csv.COLUMNS['Name']] - tags = line[cubecobra_csv.COLUMNS['Tags']].split(', ') - if 'core' in tags: - core_cards.append(card) - + csv_path = cubecobra_csv.request_cube_csv('TheElegantCube_fetched', 'elegant') + core_cards = cubecobra_csv.load_cube_names(csv_path, {'core'}) core_cards.sort() for card in core_cards: print(card) diff --git a/cube_json.py b/cube_json.py index 9254b76..602fa29 100644 --- a/cube_json.py +++ b/cube_json.py @@ -1,36 +1,9 @@ -import csv import json -import requests -from pathlib import Path -from word_count import EXPECTED_HEADER, COLUMNS, TEMP_JSON, is_actual_mtg_card +from word_count import TEMP_JSON, is_actual_mtg_card -def request_cube_csv(cube_name, cube_id): - url = f'https://cubecobra.com/cube/download/csv/{cube_id}' - url += '?primary=Color%20Category&secondary=Types-Multicolor&tertiary=CMC2' - response = requests.get(url) - filename = Path('cube_csvs', f'{cube_name}.csv') - open(filename, 'wb').write(response.content) - return filename - - -def load_cube_from_csv(filename, tag_filter=None): - cards = [] - with open(filename) as f: - reader = csv.reader(f) - header_line = next(reader) - assert header_line == EXPECTED_HEADER - for line in reader: - card = line[COLUMNS['Name']] - # Export CSV is broken on CubeCobra because Image Back URL is always just one double quotes character. - tags = [t.strip() for t in line[COLUMNS['Tags']].split(', ')] - if tag_filter is None or any(t in tag_filter for t in tags): - cards.append(card) - return cards - - -def create_cube_json(cards, output_filename=TEMP_JSON): +def create_cube_json(cards, output_filename=TEMP_JSON) -> str: # Call this once to create a smaller JSON file from the full card list # Visit https://scryfall.com/docs/api/bulk-data and look for "Oracle Cards" file; # Download and rename it to 'oracle_cards.json' @@ -49,4 +22,4 @@ def create_cube_json(cards, output_filename=TEMP_JSON): string_data = json.dumps(cube_data) f_cube_json.write(string_data) f_cube_json.close() - return output_filename \ No newline at end of file + return output_filename diff --git a/cube_pool.py b/cube_pool.py index 531d66a..723674a 100644 --- a/cube_pool.py +++ b/cube_pool.py @@ -8,80 +8,58 @@ from collections import defaultdict from typing import Dict, List, Set import cubecobra_csv - - -@dataclasses.dataclass -class Card: - name: str - color_category: str - colors: List[str] - tags: List[str] - - def __post_init__(self): - assert self.color_category in 'wubrgchml0', f'{self}' - - def __repr__(self): - return self.name +from cubecobra_csv import CubeCard class CubePool: def __init__(self): - self._categories: List[Card] = defaultdict(list) + self._categories: List[CubeCard] = defaultdict(list) - def add_card(self, card: Card): + def add_card(self, card: CubeCard): self._categories[card.color_category].append(card) - def wide_sample(self, n: int) -> List[Card]: + def wide_sample(self, n: int) -> List[CubeCard]: wide_iterator = list(itertools.chain.from_iterable(self._categories.values())) sample = random.sample(wide_iterator, n) return sample - def category_sample(self, color_category: str, n: int) -> List[Card]: + def category_sample(self, color_category: str, n: int) -> List[CubeCard]: return random.sample(self._categories[color_category], n) - def draw_from_category(self, color_category: str, n: int) -> List[Card]: + def draw_from_category(self, color_category: str, n: int) -> List[CubeCard]: drawn = self.category_sample(color_category, n) self.remove_cards(drawn, color_category) return drawn - def remove_cards(self, removed: List[Card], color_category: str): + def remove_cards(self, removed: List[CubeCard], color_category: str): existing = self._categories[color_category] self._categories[color_category] = [c for c in existing if not c in removed] - def fetch(self, card_name: str, color_category: str) -> Card: - found: List[Card] = list(filter(lambda c: c.name == card_name, self._categories[color_category])) + def fetch(self, card_name: str, color_category: str) -> CubeCard: + found: List[CubeCard] = list(filter(lambda c: c.name == card_name, self._categories[color_category])) assert len(found) >= 1 - card_taken: Card = found[0] + card_taken: CubeCard = found[0] self.remove_cards([card_taken], color_category) return card_taken - def get_category(self, color_category: str) -> List[Card]: + def get_category(self, color_category: str) -> List[CubeCard]: return self._categories[color_category] def load_pools(csv_path: str, pool_tags: Set[str]) -> Dict[str, CubePool]: pools = defaultdict(CubePool) - with open(csv_path) as f: - reader = csv.reader(f) - header_line = next(reader) - cubecobra_csv.assert_header(header_line) - for line in reader: - card_name = cubecobra_csv.get_name(line) - tags = cubecobra_csv.get_tags(line) - # print(f'{card_name} {tags}') - pool_tags_for_card = pool_tags.intersection(tags) - if len(pool_tags_for_card) == 0: - print(f'{card_name} does not have any of the tags: {pool_tags}') - elif len(pool_tags_for_card) > 1: - print(f'{card_name} should only be tagged as one in {pool_tags} ' - 'but is tagged as {pool_tags_for_card}') - else: - (pool_tag,) = pool_tags_for_card - color_category = cubecobra_csv.get_color_category(line) - # print(f'{card_name} {color_category}') - colors = cubecobra_csv.get_colors(line) - # print(f'{card_name} {colors}') - card = Card(card_name, color_category, colors, tags) - pools[pool_tag].add_card(card) + cube: List[CubeCard] = cubecobra_csv.load_cube(csv_path) + + for cube_card in cube: + pool_tags_for_card = pool_tags.intersection(cube_card.tags) + if len(pool_tags_for_card) == 0: + print(f'{cube_card.name} does not have any of the tags: {pool_tags}') + elif len(pool_tags_for_card) > 1: + print(f'{cube_card.name} should only be tagged as one in {pool_tags} ' + 'but is tagged as {pool_tags_for_card}') + else: + (pool_tag,) = pool_tags_for_card + pools[pool_tag].add_card(cube_card) + return pools diff --git a/cubecobra_csv.py b/cubecobra_csv.py index 11ba603..775540e 100644 --- a/cubecobra_csv.py +++ b/cubecobra_csv.py @@ -1,7 +1,10 @@ -# TODO: Make this an actual API +import csv +import dataclasses +from pathlib import Path from typing import List, Set +import requests EXPECTED_HEADER = [ 'Name', @@ -26,10 +29,6 @@ EXPECTED_HEADER = [ COLUMNS = {column_name: i for i, column_name in enumerate(EXPECTED_HEADER)} -def assert_header(header_line): - assert header_line == EXPECTED_HEADER - - _COLOR_CATEGORIES = { 'w': 'w', 'u': 'u', @@ -52,21 +51,72 @@ _COLOR_CATEGORIES = { } -def get_name(line: List) -> str: +@dataclasses.dataclass +class CubeCard: + name: str + color_category: str + colors: str + tags: Set[str] + + def __post_init__(self): + assert self.color_category in 'wubrgchml0', f'{self}' + + def __str__(self): + return self.name + + +def request_cube_csv(cube_name, cube_id) -> Path: + url = f'https://cubecobra.com/cube/download/csv/{cube_id}' + url += '?primary=Color%20Category&secondary=Types-Multicolor&tertiary=CMC2' + response = requests.get(url) + filename = Path('cube_csvs', f'{cube_name}.csv') + open(filename, 'wb').write(response.content) + return filename + + +def load_cube_names(filename, tag_filter: Set[str]=None) -> List[str]: + cube_cards: List[CubeCard] = load_cube(filename) + cards = [] + for cube_card in cube_cards: + if tag_filter is None or tag_filter.intersection(cube_card.tags): + cards.append(cube_card.name) + return cards + + +def load_cube(filename) -> List[CubeCard]: + cards = [] + with open(filename) as f: + reader = csv.reader(f) + header_line = next(reader) + _assert_header(header_line) + for line in reader: + card_name = _get_name(line) + tags = _get_tags(line) + color_category = _get_color_category(line) + colors = _get_colors(line) + cards.append(CubeCard(card_name, color_category, colors, tags)) + return cards + + +def _assert_header(header_line): + assert header_line == EXPECTED_HEADER + + +def _get_name(line: List) -> str: return line[COLUMNS['Name']] -def get_tags(line: List) -> Set[str]: - return set(line[COLUMNS['Tags']].split(', ')) +def _get_tags(line: List) -> Set[str]: + return set(t.strip() for t in line[COLUMNS['Tags']].split(', ')) -def get_color_category(line: List) -> str: +def _get_color_category(line: List) -> str: color_category = line[COLUMNS['Color Category']] assert color_category in _COLOR_CATEGORIES, f'color_category "{color_category}" unknown' return _COLOR_CATEGORIES[color_category] -def get_colors(line: List) -> str: +def _get_colors(line: List) -> str: colors = line[COLUMNS['Color']] #assert color_category in _COLOR_CATEGORIES, f'color_category "{color_category}" unknown' return sorted(colors.lower()) diff --git a/generate_draft_pool.py b/generate_draft_pool.py index e487069..79177d1 100644 --- a/generate_draft_pool.py +++ b/generate_draft_pool.py @@ -2,9 +2,8 @@ import hashlib -import cube_json import cube_pool - +import cubecobra_csv OUTPUT_POOL_SIZE = 360 CARDS_FROM_OCCASIONAL = 48 @@ -17,7 +16,7 @@ OUTPUT_FILENAME = 'cards_in_draft.txt' def main(): - csv_path = cube_json.request_cube_csv('TheElegantCube_fetched', 'elegant') + csv_path = cubecobra_csv.request_cube_csv('TheElegantCube_fetched', 'elegant') pools = cube_pool.load_pools(csv_path, {'core', 'occasional'}) diff --git a/rogue_duel.py b/rogue_duel.py index e589d17..6d9a0da 100644 --- a/rogue_duel.py +++ b/rogue_duel.py @@ -6,8 +6,9 @@ from collections import Counter from pathlib import Path from typing import Dict, List -import cube_json import cube_pool +import cubecobra_csv +from cubecobra_csv import CubeCard CUBE_NAME = 'TheElegantCube_fetched' @@ -15,7 +16,7 @@ CUBE_NAME = 'TheElegantCube_fetched' class RogueDuelPlayerState: def __init__(self): self.colors: List[str] = [] - self.deck: List[cube_pool.Card] = [] + self.deck: List[CubeCard] = [] class RogueDuelState: @@ -24,9 +25,9 @@ class RogueDuelState: self.player2 = RogueDuelPlayerState() self.player_states = (self.player1, self.player2) - csv_path = cube_json.request_cube_csv('TheElegantCube_fetched', 'elegant') + csv_path = cubecobra_csv.request_cube_csv('TheElegantCube_fetched', 'elegant') self.pools: Dict[str, cube_pool.CubePool] = cube_pool.load_pools(csv_path, {'core', 'occasional'}) - self.discard_pile: List[cube_pool.Card] = [] + self.discard_pile: List[CubeCard] = [] def main(): @@ -40,7 +41,7 @@ def main(): def _decide_colors(state: RogueDuelState): - sealed_pools: Dict[str, List[cube_pool.Card]] = {} + sealed_pools: Dict[str, List[CubeCard]] = {} for color_category in 'wubrg': sealed_pools[color_category] = (state.pools['core'].draw_from_category(color_category, 6) + state.pools['occasional'].draw_from_category(color_category, 2)) @@ -77,7 +78,7 @@ def _pick_a_color(colors_available: List[str], prompt: str) -> str: def _pick_artifacts(state: RogueDuelState): - artifact_pool: List[cube_pool.Card] = (state.pools['core'].draw_from_category('c', 4) + + artifact_pool: List[CubeCard] = (state.pools['core'].draw_from_category('c', 4) + state.pools['occasional'].draw_from_category('c', 1)) print(artifact_pool) card = _pick_a_card(artifact_pool, 'Player 1, choose an artifact:') @@ -87,7 +88,7 @@ def _pick_artifacts(state: RogueDuelState): state.discard_pile.extend(artifact_pool) -def _pick_a_card(cards_available: List[cube_pool.Card], prompt: str) -> cube_pool.Card: +def _pick_a_card(cards_available: List[CubeCard], prompt: str) -> CubeCard: for i, card in enumerate(cards_available, 1): print(f'[{i:>2}] {card.name}') index_chosen = -1 @@ -114,14 +115,14 @@ def _fill_with_lands(state: RogueDuelState): def _basic_for_color(color: str) -> str: name = {'w': 'Plains', 'u': 'Island', 'b': 'Swamp', 'r': 'Mountain', 'g': 'Forest'}[color] - return cube_pool.Card(name, '0', color, []) + return CubeCard(name, '0', [color], []) -def _fetch_duals(state: RogueDuelState, colors: List[str]) -> List[cube_pool.Card]: +def _fetch_duals(state: RogueDuelState, colors: List[str]) -> List[CubeCard]: colors = sorted(colors) duals = [] # Get duals - core_lands: List[cube_pool.Card] = state.pools['core'].get_category('l') + core_lands: List[CubeCard] = state.pools['core'].get_category('l') for land in core_lands: if land.colors == colors: duals.append(land) @@ -137,7 +138,7 @@ def _export_decks(state: RogueDuelState): _export_deck(player_state.deck, filepath) -def _export_deck(deck: List[cube_pool.Card], filepath: Path): +def _export_deck(deck: List[CubeCard], filepath: Path): deck_counter = Counter([card.name for card in deck]) with open(filepath, 'w') as f: for card_name, quantity in deck_counter.most_common(): diff --git a/word_count.py b/word_count.py index bd7c20b..1f99577 100644 --- a/word_count.py +++ b/word_count.py @@ -3,6 +3,7 @@ import json from collections import defaultdict import cube_json +import cubecobra_csv def load_cube_from_txt(filename): @@ -168,12 +169,12 @@ if __name__ == '__main__': # Use either option: # 1. If you have your cube in Cube Cobra and want to filter per tag - # csv_path = cube_json.request_cube_csv('TheElegantCube_fetched', 'elegant') + # csv_path = cubecobra_csv.request_cube_csv('TheElegantCube_fetched', 'elegant') # cube_list = cube_json.load_cube_from_csv(csv_path) # 2. If you have your cube in Cube Cobra and want to consider only cards with a certain tag - csv_path = cube_json.request_cube_csv('TheElegantCube_fetched', 'elegant') - cube_list = cube_json.load_cube_from_csv(csv_path, tag_filter={'core'}) + csv_path = cubecobra_csv.request_cube_csv('TheElegantCube_fetched', 'elegant') + cube_list = cubecobra_csv.load_cube_names(csv_path, tag_filter={'core'}) # 3. If you have a .txt of your cube # cube_list = cube_json.load_cube_from_txt('YourCubeHere.txt')