File size: 6,773 Bytes
14d9438
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import json
from collections import defaultdict
import torch
import numpy as np
import os
import pickle
import yaml


basics = ['Mountain', 'Forest', 'Swamp', 'Island', 'Plains']


def normalize_card_name(name):
    """Canonical card name: front face only, no A- prefix, underscores→spaces, basic variants collapsed."""
    name = name.split(' // ')[0]   # DFC: keep front face only
    name = basic_card_name(name)   # Mountain_1 → Mountain (must precede underscore replace)
    name = name.replace('_', ' ')  # CSV column artifact
    if name.startswith('A-'):
        name = name[2:]            # Alchemy rebalanced cards
    name = name.replace("Sol'kanar", "Sol'Kanar")  # capitalization mismatch in source data
    return name


def load_config(path):
    with open(path, 'r') as f:
        return yaml.safe_load(f)


def unwrap(net):
    return net.module if hasattr(net, "module") else net


def get_embedding_dict(path, add_nontransformed=False):
    with open(path, 'rb') as f:
        embedding_dict = pickle.load(f)

    if add_nontransformed:
        # Normalize all keys: deduplicate DFCs, strip A- prefixes, collapse basic variants.
        # Earlier entries win so the raw full-name embedding isn't silently dropped.
        normalized = {}
        for k, v in embedding_dict.items():
            nk = normalize_card_name(k)
            if nk not in normalized:
                normalized[nk] = v
        return normalized
    return embedding_dict

def dump_embedding_dict(embedding_dict, name, embedding_folder = ''):
    with open(name, 'wb') as f:
        pickle.dump(embedding_dict,f)

def _download_scryfall_cards(path):
    import requests
    from datetime import date
    os.makedirs(path, exist_ok=True)
    headers = {"User-Agent": "MTG-IL/1.0", "Accept": "application/json"}
    print("Fetching Scryfall bulk data index...")
    resp = requests.get("https://api.scryfall.com/bulk-data", headers=headers)
    resp.raise_for_status()
    bulk_index = resp.json()
    if "data" not in bulk_index:
        raise RuntimeError(f"Unexpected Scryfall response: {bulk_index}")
    entry = next((e for e in bulk_index["data"] if e["type"] == "default_cards"), None)
    if entry is None:
        raise RuntimeError(f"No default_cards entry. Types: {[e['type'] for e in bulk_index['data']]}")
    download_uri = entry["download_uri"]
    out_path = os.path.join(path, f"cards_{date.today()}.json")
    print(f"Downloading Scryfall card data to {out_path}...")
    with requests.get(download_uri, stream=True, headers=headers) as r:
        r.raise_for_status()
        with open(out_path, "wb") as f:
            for chunk in r.iter_content(chunk_size=1 << 17):
                f.write(chunk)
    print("Scryfall download complete.")
    return out_path


def get_card_json(path = './data/', transform = False):
    card_files = [f for f in os.listdir(path) if 'cards_' in f]
    card_files.sort(key=lambda x: os.path.getmtime(os.path.join(path, x)))
    if len(card_files) == 0:
        card_path = _download_scryfall_cards(path)
    else:
        card_path = os.path.join(path, card_files[-1])
        print(f'Using cards file: {card_path}')

    cards = json.load(open(card_path, encoding='utf8'))
    if transform:
        card_dict_split = {}
        for card in cards:
            card_dict_split[normalize_card_name(card['name'])] = card
        return card_dict_split
    return cards

def load_winrates(path):
    import csv
    with open(path, 'r') as f:
        reader = csv.reader(f)
        next(reader)
        winrates = {row[0]: float(row[15].strip('%')) if row[15] else None for row in reader}
    min_winrate = min([winrate for winrate in winrates.values() if winrate is not None])
    for card in winrates:
        if winrates[card] is None:
            winrates[card] = min_winrate
    return winrates

def clean_card(card):
    card = basic_card_name(card)
    if 'A-' in card:
        card = card.replace('A-','')
    elif "Sol'kanar" in card:
        card = card.replace("Sol'kanar","Sol'Kanar")
    elif '_' in card:
        card = card.replace('_',' ')
    return card

def basic_card_name(card_name):
    ints = ['1','2','3','4','5']
    for b in basics:
        if b in card_name:
            for i in ints:
                if card_name == f'{b}_{i}':
                    return b
    return card_name

def get_embedding_of_card(card_name, embedding_dict):
    try:
        key = normalize_card_name(card_name)
        if key in embedding_dict:
            return embedding_dict[key], False
        raise Exception(f'Could not find {card_name!r} (normalized: {key!r})')
    except Exception as e:
        print(e)
        raise e
            

def get_card_embeddings(card_names, embedding_dict, embedding_size=1330):
    embeddings = []
    for card in card_names:
        if card == '':
            embeddings.append([])
        elif card == []:
            if type(embedding_size) == tuple:
                channels, height, width = embedding_size
                new_embedding = torch.zeros(1,channels, height, width)
            else:
                new_embedding = torch.zeros(1,embedding_size)
            embeddings.append(new_embedding)

        elif isinstance(card, list):
            if len(card) == 0:
                embeddings.append(None)
                continue
            deck_embedding = []
            for c in card:
                embedding, got_new = get_embedding_of_card(c, embedding_dict)
                deck_embedding.append(embedding)
            try:
                num_cards = len(deck_embedding)
                deck_embedding = torch.stack(deck_embedding)
                if type(embedding_size) == tuple:
                    channels, height, width = embedding_size
                    deck_embedding = deck_embedding.view(num_cards,channels, height, width)
                else:
                    deck_embedding = deck_embedding.view(num_cards,-1)
            except Exception as e:
                raise e
            embeddings.append(deck_embedding)
        else:
            embedding, got_new = get_embedding_of_card(card, embedding_dict)
            embeddings.append(embedding)
    return embeddings


    




def get_original_name(transformed_name):
    cards_original = transformed_name.replace('_', ' ')
    for suffix in [' 1', ' 2', ' 3', ' 4', ' 5']:
        cards_original = cards_original.replace(suffix, '')
    if 'Nicol Bolas' in cards_original:
        cards_original = 'Nicol Bolas, the Ravager // Nicol Bolas, the Arisen'
    return cards_original