DeckLock part 2: Magic the Gathering
expanding DeckLock to support more games
DeckLock, a static-website generator to generate overviews of KeyForge decks, will be expanded to support Magic: the Gathering. To find more about DeckLock and how it is designed look at the previous post. Magic: the Gathering is a very different game compared to KeyForge. There is a large pool of cards available and players can select a set they wish to play with. There are different formats and the format determines which cards and how many of them you can play. Most formats require decks to be at least 60 cards with any number of basic lands and a maximum 4 copies of other cards. This means each player can bring a completely custom set of cards to the table and we’ll have to come up with a system to support that. Unlike KeyForge where each deck is unique and the list of cards can be found online, here we’ll include a system to add a deck list, with a short description and turn those into pages containing cards.
You can find my DeckLock instance with an overview of my KeyForge and Magic: the Gathering decks (paper and online) here.
Designing a Reader
Here we actually can use a structure analogous to Pelican’s articles. We’ll add a decklist to a designated folder, Pelican will pick up this file, the Reader class will parse it, fetch the decks cards’ information and image online and inject it into an appropriate template.
A common file format to store decks is the Magic Workstation format, which is also included in MTGTop8 (I’ll admit it, I’m netdecking most of the time). This is both machine- and human-readable, there are a number of comment lines which are used to store the name of the deck, who created it, … other lines contain the number of time a card is included in the deck, the set the card is from and the name of the card. Fields are space delimited. Sideboard cards are included the same way, but the line starts with 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
Based on the name of the card, all other information can be pulled from ScryFall, a website the has images for all cards and also includes an API that can be used to fetch useful details to include. The set, included here between square brackets is optional, if nothing is entered the most recent version is selected.
The Reader class
Below you’ll find the code for the Reader, this needs to be a class that is build upon Pelican’s BaseReader, it needs
to have a property enabled
which should be set to True and a property file_extensions
which is a list of extensions that
should be processed by this reader, in this case only .mwDeck files. Note that there are a number of helper functions in
full code that are required to make this work. Head over to the DeckLock GitHub repository to check out the full code.
Upon creation of an MTGReader the cached data will be loaded (if it exists) and create a path to store card images (if necessary).
The function read
attached to the MTGReader is required, here the metadata is set. A category and data are required by
Pelican though we won’t be using them so we just specify any value. Here the required template is also specified which
is important to ensure the data will be rendered using the correct template. The next block of code reads the .mwDeck
file, fetches card details and the image from Scryfall if it isn’t included in the cached data and creates a
dictionary that contains all data required for the deck’s page. The page title, slug (url friendly name), the url and
the path to the output file are also constructed here from the deck’s name.
The deck data along with empty content is returned, Pelican will ensure a reader is created for each .mwDeck file in the posts folder, processed and combined with a template.
Finally, you’ll need to create a function that adds the reader (here add_reader
but you are free to choose another name)
and a register
function (name is not optional here). The latter will connect the add_reader function to Pelican’s set of
readers.
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)
Updating the configuration
Just like for KeyForge a few settings need to be included in the configuration file. Even if there are no decks, this setting is required, setting this to False will remove Magic: the Gathering from the main page overview. The other parts are important to make sure the plugin knows where to find and store data.
Unlike in our previous post, the overview page for Magic makes use of Pelican’s build in functions. However, we do need to make sure this file is known to Pelican and properly handled. By adding it to TEMPLATE_PAGES the template mtg_overview.html is known to Pelican and will be rendered and stored as mtg.html.
MTG_ENABLED = True
MTG_PATH = "data"
MTG_ASSETS_PATH = "assets/mtg"
TEMPLATE_PAGES = {'mtg_overview.html': 'mtg.html'}
A Note on card images
DeckLock will download images from Scryfall when building a local version using make html
. For private use this is
permitted in most countries. However, when building a version to release on the internet including copyrighted images
might no be a smart thing to do. Most likely it even is illegal. To this end, there are a few options included in the
file publishconf.py
which is used when building the version intended for on-line use. The latter can be build with
the command make release
.
USE_EXTERNAL_LINKS = True
STATIC_EXCLUDES = ['assets/keyforge', 'assets/mtg']
USE_EXTERNAL_LINKS
will use links to external platforms for card images rather than the downloaded images. For M:tG,
Scryfall will be used, for KeyForge the official images from the master vault are used. By excluding the assets folders
for both games by setting STATIC_EXCLUDES
, we prevent Pelican from copying the downloaded image to the output intended
for hosting DeckLock online.
These settings can be changed but should obviously be used with careful consideration. Re-hosting copyrighted material might have repercussions.
Other improvements
Along with support for Magic: the Gathering a number of other improvements were made. Most notably is the inclusion of a proper logo for DeckLock. But also under the hood there are a number of improvements. Gwent has been added in [part 3].
Liked this post ? You can buy me a coffee