DeckLock, een statische-websitegenerator die overzichten van KeyForge-decks maakt, wordt uitgebreid met ondersteuning voor Magic: the Gathering. Meer informatie over DeckLock en het ontwerp ervan vind je in het vorige artikel. Magic: the Gathering verschilt sterk van KeyForge. Er is een grote verzameling kaarten beschikbaar en spelers kunnen zelf een set samenstellen waarmee ze willen spelen. Er bestaan verschillende formaten en het formaat bepaalt welke kaarten en hoeveel exemplaren ervan je mag spelen. In de meeste formaten moeten decks minstens 60 kaarten bevatten, met een onbeperkt aantal basislanden en maximaal 4 exemplaren van andere kaarten. Daardoor kan elke speler een volledig zelfgekozen kaartenverzameling meebrengen en moeten we een systeem bedenken dat dit ondersteunt. Anders dan bij KeyForge, waar elk deck uniek is en de kaartenlijst online te vinden is, voegen we hier een systeem toe om een decklijst met een korte beschrijving in te voeren en die om te zetten in pagina’s met de kaarten.

Mijn DeckLock-versie met een overzicht van mijn KeyForge- en Magic: the Gathering-decks (op papier en online) vind je hier.

Een Reader ontwerpen

Hier kunnen we een structuur gebruiken die vergelijkbaar is met de artikels van Pelican. We voegen een decklijst aan een daarvoor bestemde map toe, Pelican pikt het bestand op, de Reader-klasse verwerkt het, haalt online de informatie en afbeelding van de kaarten in het deck op en voegt alles in een geschikt sjabloon in.

Een gangbaar bestandsformaat om decks op te slaan is het Magic Workstation-formaat, dat ook in MTGTop8 wordt gebruikt (ik geef toe: meestal netdeck ik). Het is zowel machine- als mensleesbaar. Een aantal commentaarregels bewaart de naam van het deck, wie het maakte, … Andere regels bevatten hoe vaak een kaart in het deck zit, uit welke set de kaart komt en de naam ervan. De velden worden door spaties gescheiden. Kaarten uit het sideboard worden op dezelfde manier opgenomen, maar de regel begint met SB:

// NAME : 9 Land Stompy
// CREATOR : Sebastian Proost
// FORMAT : Casual
9 [USG] Forest
4 [MMQ] Land Grant
4 [ALL] Elvish Spirit Guide
3 [MMQ] Vine Dryad
...
SB:  2 [TMP] Root Maze
SB:  4 [ULG] Hidden Gibbons
SB:  3 [ONS] Naturalize
SB:  2 [MMQ] Rushwood Legate
SB:  3 [UDS] Compost

Aan de hand van de kaartnaam kan alle overige informatie van ScryFall worden gehaald, een website met afbeeldingen van alle kaarten en een API waarmee nuttige details kunnen worden opgehaald. De set, hier tussen vierkante haken, is optioneel. Als je niets invult, wordt de meest recente versie gekozen.

De Reader-klasse

Hieronder vind je de code voor de Reader. Dit moet een klasse zijn die voortbouwt op BaseReader van Pelican. Ze heeft een eigenschap enabled nodig die op True staat en een eigenschap file_extensions: een lijst met extensies die deze Reader moet verwerken, in dit geval uitsluitend .mwDeck-bestanden. Merk op dat de volledige code een aantal hulpfuncties bevat die nodig zijn om dit te laten werken. Bekijk de volledige code in de DeckLock-repository op GitHub.

Bij het maken van een MTGReader worden de gecachete gegevens geladen (als die bestaan) en wordt indien nodig een pad gemaakt om kaartafbeeldingen op te slaan.

De functie read van MTGReader is verplicht. Hier worden de metadata ingesteld. Pelican vereist een categorie en datum, maar omdat we die niet gebruiken, vullen we om het even welke waarde in. Ook het vereiste sjabloon wordt hier opgegeven. Dat is belangrijk om ervoor te zorgen dat de gegevens met het juiste sjabloon worden weergegeven. Het volgende codeblok leest het .mwDeck- bestand, haalt kaartdetails en de afbeelding op bij Scryfall als die niet in de gecachete gegevens staan, en maakt een dictionary met alle gegevens die de deckpagina nodig heeft. Ook de paginatitel, slug (URL-vriendelijke naam), URL en het pad naar het uitvoerbestand worden hier uit de decknaam opgebouwd.

De deckgegevens worden samen met lege inhoud teruggegeven. Pelican zorgt ervoor dat voor elk .mwDeck-bestand in de artikelmap een Reader wordt gemaakt, waarna het bestand wordt verwerkt en met een sjabloon gecombineerd.

Ten slotte moet je een functie maken die de Reader toevoegt (hier add_reader, maar je mag een andere naam kiezen) en een functie register (die naam is wel verplicht). Die laatste koppelt de functie add_reader aan de verzameling Readers van Pelican.

class MTGReader(BaseReader):
    enabled = True

    file_extensions = ['mwDeck']

    def __init__(self, settings):
        super(MTGReader, self).__init__(settings)

        self.cached_data = {}

        if os.path.exists(self.mtg_data_path):
            with open(self.mtg_data_path, 'r') as fin:
                self.cached_data = json.load(fin)

        Path(self.mtg_assets_cards_path(full=True)).mkdir(parents=True, exist_ok=True)

    @property
    def mtg_data_path(self):
        return os.path.join(
            self.settings.get("PATH"), self.settings.get("MTG_PATH"), "mtg.cached_cards.json"
        )

    def write_cache(self):
        with open(self.mtg_data_path, "w") as fout:
            json.dump(self.cached_data, fout, sort_keys=True, indent=4, separators=(",", ": "))

    def mtg_assets_cards_path(self, full=False):
        if full:
            return os.path.join(
                self.settings.get("PATH"), self.settings.get("MTG_ASSETS_PATH"), 'cards'
            )
        else:
            return os.path.join(
                self.settings.get("MTG_ASSETS_PATH"), 'cards'
            )

    def add_card_data(self, card_set, card_name):
        if card_set not in self.cached_data.keys():
            self.cached_data[card_set] = {}
        if card_name not in self.cached_data[card_set]:
            card_data = get_card_data(card_set, card_name)
            self.cached_data[card_set][card_name] = card_data
        else:
            card_data = self.cached_data[card_set][card_name]
        try:
            if "card_faces" in card_data.keys():
                card_data.update(card_data["card_faces"][0])

            img_url = card_data["image_uris"]["border_crop"]
            local_path = get_local_card_img_path(self.mtg_assets_cards_path(full=False), img_url)
            self.cached_data[card_set][card_name]["image_path"] = local_path

            local_path_full = get_local_card_img_path(self.mtg_assets_cards_path(full=True), img_url)
            fetch_image(img_url, local_path_full)
        except:
            print(f"an error occurred fetching {card_name} from set {card_set}")

    def read(self, filename):
        metadata = {'category': 'MTG_Deck',
                    'date': '2020-04-13',
                    'template': 'mtg_deck'
                    }

        deck_data = {
            'main': [],
            'sideboard': []
        }

        with open(filename, 'r') as fin:
            for line in fin:
                if line.startswith('//'):
                    tag, value = parse_meta(line)
                    metadata[tag.lower()] = value
                elif line.strip() != '':
                    sideboard, card_set, card_count, card_name = parse_card_line(line)
                    self.add_card_data(card_set, card_name)

                    card_data = {
                        'name': card_name,
                        'count': card_count,
                        'data': self.cached_data[card_set][card_name],
                        'card_type': parse_card_type(self.cached_data[card_set][card_name]['type_line'])
                    }

                    if sideboard:
                        deck_data['sideboard'].append(card_data)
                    else:
                        deck_data['main'].append(card_data)

        self.write_cache()

        metadata['title'] = metadata['name']
        metadata['slug'] = slugify(metadata['title'], regex_subs=self.settings.get('SLUG_REGEX_SUBSTITUTIONS', []))

        metadata['url'] = f"mtg/{metadata['format']}/{metadata['slug']}/"
        metadata['save_as'] = f"{metadata['url']}index.html"

        parsed = {}
        for key, value in metadata.items():
            parsed[key] = self.process_metadata(key, value)

        parsed['deck'] = deck_data

        return "", parsed


def add_reader(readers):
    readers.reader_classes['mwDeck'] = MTGReader


def register():
    signals.readers_init.connect(add_reader)

De configuratie bijwerken

Net als voor KeyForge moeten enkele instellingen aan het configuratiebestand worden toegevoegd. Zelfs als er geen decks zijn, is deze instelling nodig. Als je ze op False zet, verdwijnt Magic: the Gathering uit het overzicht op de hoofdpagina. De andere onderdelen zorgen ervoor dat de plugin weet waar gegevens te vinden en op te slaan zijn.

Anders dan in het vorige artikel gebruikt de overzichtspagina voor Magic de ingebouwde functies van Pelican. We moeten er wel voor zorgen dat Pelican dit bestand kent en correct verwerkt. Door het aan TEMPLATE_PAGES toe te voegen, kent Pelican het sjabloon mtg_overview.html en wordt het weergegeven en opgeslagen als mtg.html.

MTG_ENABLED = True

MTG_PATH = "data"
MTG_ASSETS_PATH = "assets/mtg"

TEMPLATE_PAGES = {'mtg_overview.html': 'mtg.html'}

Een opmerking over kaartafbeeldingen

DeckLock downloadt afbeeldingen van Scryfall wanneer je met make html een lokale versie bouwt. Voor privégebruik is dat in de meeste landen toegestaan. Wanneer je een versie voor publicatie op internet bouwt, is het echter wellicht geen goed idee om auteursrechtelijk beschermde afbeeldingen op te nemen. Waarschijnlijk is het zelfs illegaal. Daarom bevat het bestand publishconf.py, dat wordt gebruikt om de onlineversie te bouwen, enkele opties. Die versie kun je bouwen met het commando make release.

USE_EXTERNAL_LINKS = True
STATIC_EXCLUDES = ['assets/keyforge', 'assets/mtg']

Met USE_EXTERNAL_LINKS worden voor kaartafbeeldingen links naar externe platformen gebruikt in plaats van de gedownloade afbeeldingen. Voor M:tG wordt Scryfall gebruikt; voor KeyForge de officiële afbeeldingen uit de Master Vault. Door de assetmappen van beide spellen via STATIC_EXCLUDES uit te sluiten, voorkomen we dat Pelican de gedownloade afbeeldingen kopieert naar de uitvoer die bestemd is om DeckLock online te hosten.

Je kunt deze instellingen wijzigen, maar uiteraard alleen na zorgvuldige afweging. Auteursrechtelijk beschermd materiaal opnieuw hosten kan gevolgen hebben.

Andere verbeteringen

Naast ondersteuning voor Magic: the Gathering werden nog enkele andere verbeteringen aangebracht. De opvallendste is een echt logo voor DeckLock. Ook onder de motorkap veranderde er heel wat. Gwent werd in deel 3 toegevoegd.