diff --git a/forge-gui/tools/DeckConversionTools/mtgdecksnet_convert.py b/forge-gui/tools/DeckConversionTools/mtgdecksnet_convert.py old mode 100644 new mode 100755 index f46f3cc9116..d802c8a6ffa --- a/forge-gui/tools/DeckConversionTools/mtgdecksnet_convert.py +++ b/forge-gui/tools/DeckConversionTools/mtgdecksnet_convert.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Modify key directories here @@ -9,7 +9,7 @@ OUT_DECKFOLDER = "./ForgeDecks" import argparse, os, re -print("Agetian's MtgDecks.net DEC to MTG Forge Deck Converter v3.5\n") +print("Agetian's MtgDecks.net DEC to MTG Forge Deck Converter v4.0\n") parser = argparse.ArgumentParser(description="Convert MtgDecks.net DEC to Forge DCK.") @@ -40,34 +40,51 @@ nonplayable_in_deck = 0 print("Loading cards...") for root, dirs, files in os.walk(CARDSFOLDER): for name in files: - if name.find(".txt") != -1: - total_cards += 1 - fullpath = os.path.join(root, name) - cardtext = open(fullpath).read() - cardtext_lower = cardtext.lower() - cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':') - cardname = ":".join(cardname_literal[1:]).strip() - if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1): - # split card, special handling needed - cardsplittext = cardtext.replace('\r','').split('\n') - cardnames = [] - for line in cardsplittext: - if line.lower().find("name:") != -1: - cardnames.extend([line.split('\n')[0].split(':')[1]]) - cardname = " // ".join(cardnames) - if cardtext.lower().find("removedeck:all") != -1: - cardlist[cardname] = 0 - else: - cardlist[cardname] = 1 - ai_playable_cards += 1 + if name.find(".txt") != -1: + total_cards += 1 + fullpath = os.path.join(root, name) + cardtext = open(fullpath).read() + cardtext_lower = cardtext.lower() + cardname_lines = cardtext.replace('\r','').split('\n') + cardname = "" + for line in cardname_lines: + if line.strip().lower().startswith("name:"): + if line.count(':') == 1: + cardname = line.split(':')[1].strip() + break + if cardname == "": + cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':') + cardname = ":".join(cardname_literal[1:]).strip() + if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1): + # split card, special handling needed + cardsplittext = cardtext.replace('\r','').split('\n') + cardnames = [] + for line in cardsplittext: + if line.lower().find("name:") != -1: + cardnames.extend([line.split('\n')[0].split(':')[1]]) + cardname = " // ".join(cardnames) + if (cardtext_lower.find("alternatemode:modal") != -1) or (cardtext_lower.find("alternatemode: modal") != -1): + # ZNR modal card, special handling needed + cardsplittext = cardtext.replace('\r','').split('\n') + cardnames = [] + for line in cardsplittext: + if line.lower().find("name:") != -1: + cardnames.extend([line.split('\n')[0].split(':')[1]]) + cardname = cardnames[0].strip() + if cardtext.lower().find("remaideck") != -1: + cardlist[cardname] = 0 + else: + cardlist[cardname] = 1 + ai_playable_cards += 1 perc_playable = (float(ai_playable_cards) / total_cards) * 100 perc_unplayable = ((float(total_cards) - ai_playable_cards) / total_cards) * 100 print("Loaded %d cards, among them %d playable by the AI (%d%%), %d unplayable by the AI (%d%%).\n" % (total_cards, ai_playable_cards, perc_playable, total_cards - ai_playable_cards, perc_unplayable)) -re_Metadata = '^//(.*) a (.*) deck by (.*)$' +re_Metadata = '^//(.*) a (.*) deck by (.*) \(dec\) Version$' re_Metadata2 = '^//(.*) a ([A-Za-z]+) MTG deck played by (.*) in (.*) - MTGDECKS.NET.*$' +re_Metadata3 = '^//(.*) a (.*) deck by (.*)$' re_DeckID = '^([0-9]+)\.dec$' re_Maindeck = '^([0-9]+) (.*)$' re_Sideboard = '^SB:[ \t]+([0-9]+) (.*)$' @@ -79,173 +96,193 @@ badChars = ['/', '\\', '*'] print("Converting decks...") for root, dirs, files in os.walk(DECKFOLDER): for name in files: - if name.find(".dec") != -1: - print("Converting deck: " + name + "...") - deck_id = -1 - s_DeckID = re.search(re_DeckID, name) - if s_DeckID: - deck_id = s_DeckID.groups()[0] - fullpath = os.path.join(root, name) - deckdata = open(fullpath).readlines() - name = "" - creator = "" - format = "" - maindeck = [] - maindeck_cards = 0 - sideboard = [] - supported = True - deckHasUnsupportedCards = False + if name.find(".dec") != -1: + print("Converting deck: " + name + "...") + deck_id = -1 + s_DeckID = re.search(re_DeckID, name) + if s_DeckID: + deck_id = s_DeckID.groups()[0] + fullpath = os.path.join(root, name) + deckdata = open(fullpath).readlines() + name = "" + creator = "" + format = "" + maindeck = [] + maindeck_cards = 0 + sideboard = [] + supported = True + deckHasUnsupportedCards = False - for line in deckdata: - #line = line.replace("\xE1", "a") - #line = line.replace("\xFB", "u") - #line = line.replace("\xE9", "e") - line = line.replace("\xC3\x86", "AE") - line = line.replace("\xC3\xA9", "e") - line = line.replace("\xC3\xBB", "u") - line = line.replace("\xC3\xA1", "a") - line = line.replace("\xC3\xAD", "i") - #line = line.replace("Unravel the Aether", "Unravel the AEther") - #line = line.replace("Aether", "AEther") - line = line.replace("Chandra, Roaring Flame", "Chandra, Fire of Kaladesh") - line = line.replace("Nissa, Sage Animist", "Nissa, Vastwood Seer") - line = line.replace("Neck Breaker", "Breakneck Rider") - line = line.replace("\xC3\xB6", "o") - line = line.replace("\x97", "-") - line = line.replace("\x91", "'") - line = line.replace("\xFB", "u") - line = line.replace("\xFC", "u") - line = line.replace("\xC4", "A") - if line[0] != "/" and line.find(" // ") == -1: - line = line.replace("/"," // ") - timepos = line.find(" -1: - line = line[0:timepos] - isCardSupported = True - mode = 0 - s_Metadata = re.search(re_Metadata, line) - if not s_Metadata: - s_Metadata = re.search(re_Metadata2, line) - mode = 1 - if s_Metadata: - name = s_Metadata.groups()[0].strip() - format = s_Metadata.groups()[1].strip() - creator = s_Metadata.groups()[2].strip() - event = "" - if mode == 1: - event = s_Metadata.groups()[3].strip() - for badChar in badChars: - event = event.replace(badChar, "-") - if args.t and args.t != format: - print("Skipping an off-format deck " + name + " (format = " + format + ")") - supported = False - continue - s_Maindeck = re.search(re_Maindeck, line) - if s_Maindeck: - cardAmount = s_Maindeck.groups()[0].strip() - cardName = s_Maindeck.groups()[1].strip() - if not cardlist.has_key(cardName) and not cardlist.has_key(cardName.replace("Aether", "AEther")) and not cardlist.has_key(cardName.replace("AEther", "Aether")): - print("Unsupported card (MAIN): " + cardName) - if args.f: - supported = False - else: - isCardSupported = False - deckHasUnsupportedCards = True - if not cardName in unsupportedList: - unsupportedList.extend([cardName]) - if cardlist.has_key(cardName): - mdline = cardAmount + " " + cardName - elif cardlist.has_key(cardName.replace("Aether", "AEther")): - mdline = cardAmount + " " + cardName.replace("Aether", "AEther") - elif cardlist.has_key(cardName.replace("AEther", "Aether")): - mdline = cardAmount + " " + cardName.replace("AEther", "Aether") - else: - mdline = cardAmount + " " + cardName # for the purposes of unsupported cards - if isCardSupported: - maindeck.extend([mdline]) - else: - maindeck.extend(["#"+mdline]) - maindeck_cards += int(cardAmount) - continue - s_Sideboard = re.search(re_Sideboard, line) - if s_Sideboard: - cardAmount = s_Sideboard.groups()[0].strip() - cardName = s_Sideboard.groups()[1].strip() - if not cardlist.has_key(cardName) and not cardlist.has_key(cardName.replace("Aether", "AEther")) and not cardlist.has_key(cardName.replace("AEther", "Aether")): - print("Unsupported card (SIDE): " + cardName) - if args.f: - supported = False - else: - isCardSupported = False - deckHasUnsupportedCards = True - if not cardName in unsupportedList: - unsupportedList.extend([cardName]) - if cardlist.has_key(cardName): - sdline = cardAmount + " " + cardName - elif cardlist.has_key(cardName.replace("Aether", "AEther")): - sdline = cardAmount + " " + cardName.replace("Aether", "AEther") - elif cardlist.has_key(cardName.replace("AEther", "Aether")): - sdline = cardAmount + " " + cardName.replace("AEther", "Aether") - else: - sdline = cardAmount + " " + cardName # for the purposes of unsupported cards - if isCardSupported: - sideboard.extend([sdline]) - else: - sideboard.extend(["#"+sdline]) - continue + for line in deckdata: + #line = line.replace("\xE1", "a") + #line = line.replace("\xFB", "u") + #line = line.replace("\xE9", "e") + line = line.replace("\xC3\x86", "AE") + line = line.replace("\xC3\xA9", "e") + line = line.replace("\xC3\xBB", "u") + line = line.replace("\xC3\xA1", "a") + line = line.replace("\xC3\xAD", "i") + #line = line.replace("Unravel the Aether", "Unravel the AEther") + #line = line.replace("Aether", "AEther") + line = line.replace("Chandra, Roaring Flame", "Chandra, Fire of Kaladesh") + line = line.replace("Lurrus of the Dream Den", "Lurrus of the Dream-Den") + line = line.replace("Nissa, Sage Animist", "Nissa, Vastwood Seer") + line = line.replace("Neck Breaker", "Breakneck Rider") + line = line.replace("Avacyn, the Purifier", "Archangel Avacyn") + line = line.replace("Dandân", "Dandan") + line = line.replace("Séance", "Seance") + line = line.replace("Jötun Grunt", "Jotun Grunt") + line = line.replace("Ifh-Bíff Efreet", "Ifh-Biff Efreet") + line = line.replace("Juzám Djinn", "Juzam Djinn") + line = line.replace("\xC3\xB6", "o") + line = line.replace("\x97", "-") + line = line.replace("\x91", "'") + line = line.replace("\xFB", "u") + line = line.replace("\xFC", "u") + line = line.replace("\xC4", "A") + if line[0] != "/" and line.find(" // ") == -1: + line = line.replace("/"," // ") + timepos = line.find(" -1: + line = line[0:timepos] + isCardSupported = True + mode = 0 + s_Metadata = re.search(re_Metadata, line) + if not s_Metadata: + s_Metadata = re.search(re_Metadata2, line) + mode = 1 + if not s_Metadata: + s_Metadata = re.search(re_Metadata3, line) + mode = 0 + if s_Metadata: + name = s_Metadata.groups()[0].strip() + format = s_Metadata.groups()[1].strip() + creator = s_Metadata.groups()[2].strip() + event = "" + if mode == 1: + event = s_Metadata.groups()[3].strip() + for badChar in badChars: + event = event.replace(badChar, "-") + if args.t and args.t != format: + print("Skipping an off-format deck " + name + " (format = " + format + ")") + supported = False + continue + s_Maindeck = re.search(re_Maindeck, line) + if s_Maindeck: + cardAmount = s_Maindeck.groups()[0].strip() + cardName = s_Maindeck.groups()[1].strip() + if cardName == "": + continue + altModalKey = cardName.split(" // ")[0].strip() + if not cardName in cardlist.keys() and not cardName.replace("Aether", "AEther") in cardlist.keys() and not cardName.replace("AEther", "Aether") in cardlist.keys() and not altModalKey in cardlist.keys(): + print("Unsupported card (MAIN): " + cardName) + if args.f: + supported = False + else: + isCardSupported = False + deckHasUnsupportedCards = True + if not cardName in unsupportedList: + unsupportedList.extend([cardName]) + if altModalKey in cardlist.keys(): + mdline = cardAmount + " " + altModalKey # ZNR modal cards with // + elif cardName in cardlist.keys(): + mdline = cardAmount + " " + cardName + elif cardName.replace("Aether", "AEther") in cardlist.keys(): + mdline = cardAmount + " " + cardName.replace("Aether", "AEther") + elif cardName.replace("AEther", "Aether") in cardlist.keys(): + mdline = cardAmount + " " + cardName.replace("AEther", "Aether") + else: + mdline = cardAmount + " " + cardName # for the purposes of unsupported cards + if isCardSupported: + maindeck.extend([mdline]) + else: + maindeck.extend(["#"+mdline]) + maindeck_cards += int(cardAmount) + continue + s_Sideboard = re.search(re_Sideboard, line) + if s_Sideboard: + cardAmount = s_Sideboard.groups()[0].strip() + cardName = s_Sideboard.groups()[1].strip() + if cardName == "": + continue + altModalKey = cardName.split(" // ")[0].strip() + if not cardName in cardlist.keys() and not cardName.replace("Aether", "AEther") in cardlist.keys() and not cardName.replace("AEther", "Aether") in cardlist.keys() and not altModalKey in cardlist.keys(): + print("Unsupported card (SIDE): " + cardName) + if args.f: + supported = False + else: + isCardSupported = False + deckHasUnsupportedCards = True + if not cardName in unsupportedList: + unsupportedList.extend([cardName]) + if altModalKey in cardlist.keys(): + sdline = cardAmount + " " + altModalKey # ZNR modal cards with // + elif cardName in cardlist.keys(): + sdline = cardAmount + " " + cardName + elif cardName.replace("Aether", "AEther") in cardlist.keys(): + sdline = cardAmount + " " + cardName.replace("Aether", "AEther") + elif cardName.replace("AEther", "Aether") in cardlist.keys(): + sdline = cardAmount + " " + cardName.replace("AEther", "Aether") + else: + sdline = cardAmount + " " + cardName # for the purposes of unsupported cards + if isCardSupported: + sideboard.extend([sdline]) + else: + sideboard.extend(["#"+sdline]) + continue - # convert here - if supported and len(maindeck) > 0: - if creator != "": - deckname = creator + " - " + name + " (" - else: - deckname = name + " (" - deckname += format - if args.i and deck_id > -1: - if format != "": + # convert here + if supported and len(maindeck) > 0: + if creator != "": + deckname = creator + " - " + name + " (" + else: + deckname = name + " (" + deckname += format + if args.i and int(deck_id) > -1: + if format != "": deckname += ", #" + deck_id - else: - deckname += "#" + deck_id - deckname += ")" - #deckname = (c for c in deckname if ord(c) < 128) - if not args.U: - deckname = re.sub(r'[^\x00-\x7F]', '@', deckname) - deckname = re.sub(r'[/\\]', '-', deckname) - deckname = re.sub(r'[?*]', '_', deckname) - if args.w and deckHasUnsupportedCards: - deckname += " [!]" - if args.D: - if deckHasUnsupportedCards: - outname = "Unsupported" + "/" + format + "/" - pathToUnsupported = "./" + OUT_DECKFOLDER + "/Unsupported/" + format - if not os.path.isdir(pathToUnsupported): - os.makedirs(pathToUnsupported) - else: - outname = format + "/" - if not os.path.isdir("./" + OUT_DECKFOLDER + "/" + format): - os.makedirs("./" + OUT_DECKFOLDER + "/" + format) - else: - outname = "" - outname += deckname + ".dck" - print ("Writing converted deck: " + outname) - dck = open(OUT_DECKFOLDER + "/" + outname, "w") + else: + deckname += "#" + deck_id + deckname += ")" + #deckname = (c for c in deckname if ord(c) < 128) + if not args.U: + deckname = re.sub(r'[^\x00-\x7F]', '@', deckname) + deckname = re.sub(r'[/\\]', '-', deckname) + deckname = re.sub(r'[?*]', '_', deckname) + if args.w and deckHasUnsupportedCards: + deckname += " [!]" + if args.D: + if deckHasUnsupportedCards: + outname = "Unsupported" + "/" + format + "/" + pathToUnsupported = "./" + OUT_DECKFOLDER + "/Unsupported/" + format + if not os.path.isdir(pathToUnsupported): + os.makedirs(pathToUnsupported) + else: + outname = format + "/" + if not os.path.isdir("./" + OUT_DECKFOLDER + "/" + format): + os.makedirs("./" + OUT_DECKFOLDER + "/" + format) + else: + outname = "" + outname += deckname + ".dck" + print ("Writing converted deck: " + outname) + dck = open(OUT_DECKFOLDER + "/" + outname, "w") - if event: - dck.write("#EVENT:"+event+"\n") + if event: + dck.write("#EVENT:"+event+"\n") - dck.write("[metadata]\n") - dck.write("Name="+deckname+"\n") - dck.write("[general]\n") - dck.write("Constructed\n") - dck.write("[Main]\n") - for m in maindeck: - dck.write(m+"\n") - if not (format == "Commander" and len(sideboard) == 1): - dck.write("[Sideboard]\n") - else: - dck.write("[Commander]\n") - for s in sideboard: - dck.write(s+"\n") + dck.write("[metadata]\n") + dck.write("Name="+deckname+"\n") + dck.write("[general]\n") + dck.write("Constructed\n") + dck.write("[Main]\n") + for m in maindeck: + dck.write(m+"\n") + if not ((format == "Commander" or format == "Brawl" or format == "Duel-Commander" or format == "Historic-Brawl" or format == "Archon") and len(sideboard) == 1): + dck.write("[Sideboard]\n") + else: + dck.write("[Commander]\n") + for s in sideboard: + dck.write(s+"\n") # write out unsupported cards log = open("dec2forge.log", "w") diff --git a/forge-gui/tools/DeckConversionTools/mtgdecksnet_decksort.py b/forge-gui/tools/DeckConversionTools/mtgdecksnet_decksort.py old mode 100644 new mode 100755 index e8957975c9e..86a2b5d4240 --- a/forge-gui/tools/DeckConversionTools/mtgdecksnet_decksort.py +++ b/forge-gui/tools/DeckConversionTools/mtgdecksnet_decksort.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 CARDSFOLDER = "../../res/cardsfolder" EDITIONS = "../../res/editions" @@ -6,10 +6,11 @@ DECKFOLDER = "." import argparse, os, re, shutil -print("Agetian's MTG Forge Deck Sorter v1.4a\n") +print("Agetian's MTG Forge Deck Sorter v2.0\n") parser = argparse.ArgumentParser(description="Sort decks into folders (by edition).") parser.add_argument("-d", action="store_true", help="physically delete original (unsorted) decks") +parser.add_argument("-x", action="store_true", help="exclude sorting by event") args = parser.parse_args() @@ -44,26 +45,34 @@ re_Card = '^[0-9]* *[A-Z] (.*)$' print("Loading cards...") for root, dirs, files in os.walk(CARDSFOLDER): for name in files: - if name.find(".txt") != -1: - total_cards += 1 - fullpath = os.path.join(root, name) - cardtext = open(fullpath).read() - cardtext_lower = cardtext.lower() - cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':') - cardname = ":".join(cardname_literal[1:]).strip() - if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1): - # split card, special handling needed - cardsplittext = cardtext.replace('\r','').split('\n') - cardnames = [] - for line in cardsplittext: - if line.lower().find("name:") != -1: - cardnames.extend([line.split('\n')[0].split(':')[1]]) - cardname = " // ".join(cardnames) - if cardtext.lower().find("removedeck:all") != -1: - cardlist[cardname] = 0 - else: - cardlist[cardname] = 1 - ai_playable_cards += 1 + if name.find(".txt") != -1: + total_cards += 1 + fullpath = os.path.join(root, name) + cardtext = open(fullpath).read() + cardtext_lower = cardtext.lower() + cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':') + cardname = ":".join(cardname_literal[1:]).strip() + if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1): + # split card, special handling needed + cardsplittext = cardtext.replace('\r','').split('\n') + cardnames = [] + for line in cardsplittext: + if line.lower().find("name:") != -1: + cardnames.extend([line.split('\n')[0].split(':')[1]]) + cardname = " // ".join(cardnames) + if (cardtext_lower.find("alternatemode:modal") != -1) or (cardtext_lower.find("alternatemode: modal") != -1): + # ZNR modal card, special handling needed + cardsplittext = cardtext.replace('\r','').split('\n') + cardnames = [] + for line in cardsplittext: + if line.lower().find("name:") != -1: + cardnames.extend([line.split('\n')[0].split(':')[1]]) + cardname = cardnames[0].strip() + if cardtext.lower().find("remaideck") != -1: + cardlist[cardname] = 0 + else: + cardlist[cardname] = 1 + ai_playable_cards += 1 perc_playable = (float(ai_playable_cards) / total_cards) * 100 perc_unplayable = ((float(total_cards) - ai_playable_cards) / total_cards) * 100 @@ -73,58 +82,59 @@ print("Loaded %d cards, among them %d playable by the AI (%d%%), %d unplayable b print("Loading editions...") for root, dirs, files in os.walk(EDITIONS): for name in files: - if name.find(".txt") != -1: - total_editions += 1 - fullpath = os.path.join(root, name) - edition = open(fullpath).readlines() - foundCards = False - code = "" - date = "" - etype = "" - name = "" - for line in edition: - line = line.replace("\r\n","") - if not foundCards: - if line == "[cards]": - foundCards = True - else: - s_Code = re.search(re_Code, line) - if s_Code: - code = s_Code.groups()[0] - #print("Code found: " + code) - s_Date = re.search(re_Date, line) - if s_Date: - date = s_Date.groups()[0] - #print("Date found: " + date) - s_Date2 = re.search(re_Date2, line) - if s_Date2: - date = s_Date2.groups()[0] + "-01" - #print("Date found: " + date) - s_Type = re.search(re_Type, line) - if s_Type: - etype = s_Type.groups()[0] - #print("Type found: " + etype) - s_Name = re.search(re_Name, line) - if s_Name: - name = s_Name.groups()[0] - #print("Name found: " + name) - else: - if etype != "Expansion" and etype != "Core" and etype != "Starter": - #print("NOT LOADING: " + code) - continue - else: - if editions.keys().count(code) == 0: - editions[code] = date - edition_names[code] = name - #print(editions) - s_Card = re.search(re_Card, line) - if s_Card: - card = s_Card.groups()[0].strip() - #print("Card found: " + card) - if cards_by_edition.keys().count(card) == 0: - cards_by_edition[card] = [] - cards_by_edition[card].append(code) - + if name.find(".txt") != -1: + total_editions += 1 + fullpath = os.path.join(root, name) + edition = open(fullpath).readlines() + foundCards = False + code = "" + date = "" + etype = "" + name = "" + for line in edition: + line = line.replace("\r\n","") + line = line.split(" @")[0] + if not foundCards: + if line.find("[cards]") != -1: + foundCards = True + else: + s_Code = re.search(re_Code, line) + if s_Code: + code = s_Code.groups()[0] + #print("Code found: " + code) + s_Date = re.search(re_Date, line) + if s_Date: + date = s_Date.groups()[0] + #print("Date found: " + date) + s_Date2 = re.search(re_Date2, line) + if s_Date2: + date = s_Date2.groups()[0] + "-01" + #print("Date found: " + date) + s_Type = re.search(re_Type, line) + if s_Type: + etype = s_Type.groups()[0] + #print("Type found: " + etype) + s_Name = re.search(re_Name, line) + if s_Name: + name = s_Name.groups()[0] + #print("Name found: " + name) + else: + if etype != "Expansion" and etype != "Core" and etype != "Starter" and code != "VOC" and code != "MIC" and code != "AFC": + #print("NOT LOADING: " + code) + continue + else: + if not code in editions.keys(): + editions[code] = date + edition_names[code] = name + #print(editions) + s_Card = re.search(re_Card, line) + if s_Card: + card = s_Card.groups()[0].strip() + #print("Card found: " + card) + if not card in cards_by_edition.keys(): + cards_by_edition[card] = [] + cards_by_edition[card].append(code) + print("Loaded " + str(len(editions)) + " editions.") @@ -132,47 +142,47 @@ def get_latest_set_for_card(card): cdate = "0000-00-00" edition = "XXX" if ignore_cards.count(card) != 0: - return "LEA" - if cards_by_edition.keys().count(card) == 0: - #print("Warning: couldn't determine an edition for card: " + card) - return "LEA" + return "LEA" + if not card in cards_by_edition.keys(): + #print("Warning: couldn't determine an edition for card: " + card) + return "LEA" for code in cards_by_edition[card]: - if editions[code] > cdate: - cdate = editions[code] - edition = code + if editions[code] > cdate: + cdate = editions[code] + edition = code return edition - + def get_earliest_set_for_card(card): cdate = "9999-99-99" edition = "XXX" - if cards_by_edition.keys().count(card) == 0: - #print("Warning: couldn't determine an edition for card: " + card) - return "LEA" + if not card in cards_by_edition.keys(): + #print("Warning: couldn't determine an edition for card: " + card) + return "LEA" for code in cards_by_edition[card]: - if editions[code] < cdate: - cdate = editions[code] - edition = code + if editions[code] < cdate: + cdate = editions[code] + edition = code return edition def get_latest_set_for_deck(deck): edition = "LEA" for line in deck.split('\n'): - #print("Line: " + line) - regexobj = re.search('^([0-9]+) +([^|]+)', line) - if regexobj: - cardname = regexobj.groups()[1].replace('\n','').replace('\r','').strip() - earliest_for_card = get_earliest_set_for_card(cardname) - #print("Card: " + cardname + ", latest for it: " + latest_for_card) - if editions[earliest_for_card] > editions[edition]: - edition = earliest_for_card + #print("Line: " + line) + regexobj = re.search('^([0-9]+) +([^|]+)', line) + if regexobj: + cardname = regexobj.groups()[1].replace('\n','').replace('\r','').strip() + earliest_for_card = get_earliest_set_for_card(cardname) + #print("Card: " + cardname + ", latest for it: " + latest_for_card) + if editions[earliest_for_card] > editions[edition]: + edition = earliest_for_card return edition def get_event_for_deck(deck): evline = deck.split('\n')[0].split("#EVENT:") if len(evline) == 2: - return evline[1] + return evline[1] else: - return "" + return "" #print(cards_by_edition["Fireball"]) #print(edition_names[get_latest_set_for_card("Fireball")]) @@ -189,18 +199,20 @@ def get_event_for_deck(deck): print("Scanning decks...") for root, dirs, files in os.walk(DECKFOLDER): for name in files: - if name.find(".dck") != -1: - total_decks += 1 - fullpath = os.path.join(root, name) - deckdata = open(fullpath).read() - set_for_deck = edition_names[get_latest_set_for_deck(deckdata)] - event_for_deck = get_event_for_deck(deckdata) - if event_for_deck != "" and event_for_deck[len(event_for_deck)-1] == ".": - event_for_deck = event_for_deck[0:len(event_for_deck)-1] - print("Deck: " + name + ", Set: " + set_for_deck + ", Event: " + event_for_deck) - if not os.access(os.path.join(root, set_for_deck, event_for_deck), os.F_OK): - os.makedirs(os.path.join(root, set_for_deck, event_for_deck)) - shutil.copy(fullpath, os.path.join(root, set_for_deck, event_for_deck, name)) - if args.d: - os.remove(fullpath) + if name.find(".dck") != -1: + total_decks += 1 + fullpath = os.path.join(root, name) + deckdata = open(fullpath).read() + set_for_deck = edition_names[get_latest_set_for_deck(deckdata)] + event_for_deck = get_event_for_deck(deckdata) + if args.x: + event_for_deck = "" + if event_for_deck != "" and event_for_deck[len(event_for_deck)-1] == ".": + event_for_deck = event_for_deck[0:len(event_for_deck)-1] + print("Deck: " + name + ", Set: " + set_for_deck + ", Event: " + event_for_deck) + if not os.access(os.path.join(root, set_for_deck, event_for_deck), os.F_OK): + os.makedirs(os.path.join(root, set_for_deck, event_for_deck)) + shutil.copy(fullpath, os.path.join(root, set_for_deck, event_for_deck, name)) + if args.d: + os.remove(fullpath) diff --git a/forge-gui/tools/DeckConversionTools/mtgdecksnet_decksort_EDH.py b/forge-gui/tools/DeckConversionTools/mtgdecksnet_decksort_EDH.py old mode 100644 new mode 100755 index 3427556b675..075fba7f3ef --- a/forge-gui/tools/DeckConversionTools/mtgdecksnet_decksort_EDH.py +++ b/forge-gui/tools/DeckConversionTools/mtgdecksnet_decksort_EDH.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 CARDSFOLDER = "../../res/cardsfolder" EDITIONS = "../../res/editions" @@ -6,10 +6,11 @@ DECKFOLDER = "." import argparse, os, re, shutil -print("Agetian's MTG Forge Deck Sorter v1.4a\n") +print("Agetian's MTG Forge Deck Sorter v2.0\n") parser = argparse.ArgumentParser(description="Sort decks into folders (by edition).") parser.add_argument("-d", action="store_true", help="physically delete original (unsorted) decks") +parser.add_argument("-x", action="store_true", help="exclude sorting by event") args = parser.parse_args() @@ -59,7 +60,15 @@ for root, dirs, files in os.walk(CARDSFOLDER): if line.lower().find("name:") != -1: cardnames.extend([line.split('\n')[0].split(':')[1]]) cardname = " // ".join(cardnames) - if cardtext.lower().find("removedeck:all") != -1: + if (cardtext_lower.find("alternatemode:modal") != -1) or (cardtext_lower.find("alternatemode: modal") != -1): + # ZNR modal card, special handling needed + cardsplittext = cardtext.replace('\r','').split('\n') + cardnames = [] + for line in cardsplittext: + if line.lower().find("name:") != -1: + cardnames.extend([line.split('\n')[0].split(':')[1]]) + cardname = cardnames[0].strip() + if cardtext.lower().find("remaideck") != -1: cardlist[cardname] = 0 else: cardlist[cardname] = 1 @@ -84,8 +93,9 @@ for root, dirs, files in os.walk(EDITIONS): name = "" for line in edition: line = line.replace("\r\n","") + line = line.split(" @")[0] if not foundCards: - if line == "[cards]": + if line.find("[cards]") != -1: foundCards = True else: s_Code = re.search(re_Code, line) @@ -109,14 +119,14 @@ for root, dirs, files in os.walk(EDITIONS): name = s_Name.groups()[0] #print("Name found: " + name) else: - if etype != "Expansion" and etype != "Core" and etype != "Starter" and etype != "Other": + if etype != "Expansion" and etype != "Core" and etype != "Starter" and etype != "Commander": #print("NOT LOADING: " + code) continue if code == "EXP" or code == "MPS": #print("NOT LOADING: " + code) - continue + continue else: - if editions.keys().count(code) == 0: + if not code in editions.keys(): editions[code] = date edition_names[code] = name #print(editions) @@ -124,7 +134,7 @@ for root, dirs, files in os.walk(EDITIONS): if s_Card: card = s_Card.groups()[0].strip() #print("Card found: " + card) - if cards_by_edition.keys().count(card) == 0: + if not card in cards_by_edition.keys(): cards_by_edition[card] = [] cards_by_edition[card].append(code) @@ -136,7 +146,7 @@ def get_latest_set_for_card(card): edition = "XXX" if ignore_cards.count(card) != 0: return "LEA" - if cards_by_edition.keys().count(card) == 0: + if not card in cards_by_edition.keys(): #print("Warning: couldn't determine an edition for card: " + card) return "LEA" for code in cards_by_edition[card]: @@ -148,7 +158,7 @@ def get_latest_set_for_card(card): def get_earliest_set_for_card(card): cdate = "9999-99-99" edition = "XXX" - if cards_by_edition.keys().count(card) == 0: + if not card in cards_by_edition.keys(): #print("Warning: couldn't determine an edition for card: " + card) return "LEA" for code in cards_by_edition[card]: @@ -198,8 +208,10 @@ for root, dirs, files in os.walk(DECKFOLDER): deckdata = open(fullpath).read() set_for_deck = edition_names[get_latest_set_for_deck(deckdata)] event_for_deck = get_event_for_deck(deckdata) - if event_for_deck != "" and event_for_deck[len(event_for_deck)-1] == ".": - event_for_deck = event_for_deck[0:len(event_for_deck)-1] + if args.x: + event_for_deck = "" + if event_for_deck != "" and event_for_deck[len(event_for_deck)-1] == ".": + event_for_deck = event_for_deck[0:len(event_for_deck)-1] print("Deck: " + name + ", Set: " + set_for_deck + ", Event: " + event_for_deck) if not os.access(os.path.join(root, set_for_deck, event_for_deck), os.F_OK): os.makedirs(os.path.join(root, set_for_deck, event_for_deck)) diff --git a/forge-gui/tools/ai_limitedplayable.lst b/forge-gui/tools/ai_limitedplayable.lst new file mode 100644 index 00000000000..342ae96ded0 --- /dev/null +++ b/forge-gui/tools/ai_limitedplayable.lst @@ -0,0 +1,572 @@ +Relic Crush +Energy Vortex +Nature's Chosen +Game of Chaos +Scarecrow +Mystic Gate +Survival of the Fittest +Relic Ward +Desolation +Firespout +Invert the Skies +Khalni Gem +Duergar Assailant +Orcish Farmer +Farrelite Priest +Heart Wolf +Infernal Darkness +Silvergill Douser +Meloku the Clouded Mirror +Active Volcano +Oran-Rief Recluse +Pale Wayfarer +Kjeldoran Warrior +Lim-Dul's Vault +Sphinx's Tutelage +Wall of Shadows +High Market +AEthermage's Touch +Homicidal Seclusion +Doomsday +Urborg +Crag Puca +Snakeform +Brass-Talon Chimera +Sacred Mesa +Mystic Veil +Ravenous Vampire +Hunting Wilds +Sudden Reclamation +Maze's End +Primordial Ooze +Ritual of the Machine +Nihilistic Glee +Spectral Guardian +Oona's Prowler +Blasting Station +Rust +Roar of the Crowd +Sporeback Troll +Gravebind +Fire and Brimstone +Gitaxian Probe +Celestial Prism +Teferi's Veil +Urza's Avenger +Fortified Area +Kjeldoran Dead +Proteus Staff +Necrotic Ooze +Relentless Assault +Diamond Valley +Elsewhere Flask +Greenseeker +Part Water +Mox Diamond +Mul Daya Channelers +Phyrexia's Core +Claws of Gix +Tolaria +Goblin Warrens +Beast Within +Naked Singularity +Pili-Pala +Manabond +Quiet Speculation +Johan +Norritt +Momentous Fall +Stonybrook Angler +Shelkin Brownie +Distant Melody +Psionic Entity +Kjeldoran Knight +Hyperion Blacksmith +Vampirism +Zombie Infestation +Wall of Vapor +Goblin Kites +Shielded Passage +Tin-Wing Chimera +Phantasmal Sphere +Juniper Order Druid +Devoted Druid +Kjeldoran Phalanx +Halls of Mist +Wildfire +Steel Golem +Battering Ram +Stromgald Spy +Barrin, Master Wizard +Sunglasses of Urza +Mana Clash +Oath of Lim-Dul +Peacekeeper +Strongarm Tactics +Birthing Pod +Splintering Wind +Shields of Velis Vel +Touch of Vitae +Swan Song +Mind Bomb +Civic Guildmage +Patriarch's Bidding +Spectral Cloak +Nettling Imp +Mirror Entity +Puresight Merrow +Illuminated Folio +Grazing Kelpie +Cadaverous Bloom +Endless Whispers +Wall of Shields +Portent +Leech Bonder +Goblin Bombardment +Aleatory +Piety +Brand of Ill Omen +Thelonite Monk +Enduring Renewal +School of the Unseen +Thorntooth Witch +Carrion Rats +Tideshaper Mystic +Transmute Artifact +Hammerheim +Flash +Shimmer +Memory Jar +Tidal Control +Timber Wolves +Unerring Sling +Phyrexian Purge +Wave Elemental +Beast Walkers +Iname, Death Aspect +Qasali Pridemage +Darkheart Sliver +Fire-Lit Thicket +Ogre Marauder +Merchant Scroll +Krovikan Plague +Telim'Tor's Edict +Divine Reckoning +Thieves' Fortune +Lammastide Weave +Confusion in the Ranks +Ancestral Statue +Repel Intruders +Limited Resources +Fate Transfer +Droning Bureaucrats +Thada Adel, Acquisitor +Mana Flare +Voltaic Key +Night of Souls' Betrayal +Phantasmal Mount +Black Carriage +Tainted AEther +Horror of Horrors +Glare of Subdual +Flooded Grove +Consuming Ferocity +Lead-Belly Chimera +Mire Shade +Surprise Deployment +Nivmagus Elemental +Bone Shaman +Orcish Lumberjack +Mana Prism +Knights of Thorn +Porphyry Nodes +Rally +Vivisection +Tropical Storm +Personal Incarnation +Daughter of Autumn +Sandals of Abdallah +Life Matrix +Inside Out +Dwarven Ruins +Rugged Prairie +Chainbreaker +Truce +Heartless Summoning +Waiting in the Weeds +Chaos Lord +Goblin Bomb +Camel +Balduvian Trading Post +Resistance Fighter +Noble Elephant +The Chain Veil +Glyph of Destruction +Library of Lat-Nam +Magus of the Unseen +Think Tank +Ventifact Bottle +Cerulean Wisps +Scrying Sheets +River's Grasp +Sunken Ruins +Elven Rite +Vexing Shusher +Dark Sphere +Chaos Moon +Meadowboon +Deadly Wanderings +Natural Selection +Vexing Arcanix +Zur's Weirding +Psychosis Crawler +Diplomacy of the Wastes +Thrull Wizard +Juxtapose +Stream of Unconsciousness +Musician +Bone Mask +Grim Feast +Orcish Librarian +Inkfathom Witch +Turtleshell Changeling +Fell the Mighty +Island Sanctuary +Bathe in Light +Tidal Wave +Time Spiral +Scarscale Ritual +Gargantuan Gorilla +Enervate +Reality Twist +Harabaz Druid +March of Souls +Orcish Settlers +Lake of the Dead +Grid Monitor +Unfulfilled Desires +War Barge +Diminishing Returns +Final Fortune +Shrewd Hatchling +Batwing Brume +Foresight +Dawnfluke +Ebon Praetor +Putrid Leech +Benevolent Offering +Heat Wave +Eye of Singularity +Drafna's Restoration +Shauku, Endbringer +Hell Swarm +Shield Bearer +Harbinger of Night +Krosan Archer +Mana Short +Lotus Vale +Misinformation +Endure +Vampiric Rites +Ritual of Subdual +Soulbright Flamekin +Mark for Death +Essence Flare +Mossbridge Troll +Woeleecher +Relic Bind +Thundercloud Shaman +AEther Barrier +Iceberg +Kjeldoran Escort +Seedling Charm +Fossil Find +Panic +Fork +Dark Privilege +Soldevi Digger +Revelsong Horn +Ivory Charm +Aladdin's Lamp +Ward of Lights +Yawgmoth Demon +Ancestral Memories +Hecatomb +Amber Prison +Gabriel Angelfire +Helm of Obedience +Mind Warp +Fiery Justice +Flooded Shoreline +Razor Pendulum +Lat-Nam's Legacy +Phyrexian Portal +Urborg Panther +Dragon Mask +Sundering Titan +Grollub +Niveous Wisps +Bone Flute +Teremko Griffin +Fungal Behemoth +Dwarven Armory +Teetering Peaks +Whip Vine +Ovinomancer +Stronghold Assassin +Cocoon +Infuse +Springleaf Drum +Ophidian +Unwilling Recruit +Dwarven Armorer +Priest of Yawgmoth +Mana Crypt +Bazaar of Baghdad +Aggression +Benalish Hero +Elkin Lair +Soldevi Sage +Jade Statue +Ambush +Twilight Mire +Wooded Bastion +Strategic Planning +Wheel of Fate +Foreshadow +Malevolent Awakening +Dwarven Sea Clan +Dire Wolves +Phyrexian Boon +Soldevi Golem +Diamond Kaleidoscope +Crystal Vein +Jandor's Saddlebags +Tar Pitcher +Null Brooch +Gaea's Liege +Forbidden Alchemy +Recall +Squandered Resources +Ancient Tomb +Varchild's Crusader +Spell Syphon +Daghatar the Adamant +Pikemen +Tornado +Scarwood Bandits +Esper Charm +Lignify +Goblin Ski Patrol +Shrieking Drake +Errand of Duty +Whirlpool Whelm +Avalanche +Reinforcements +Misfortune +Cursecatcher +Indestructible Aura +Grave Servitude +Fire Sprites +Time Reversal +Personal Tutor +Kaervek's Spite +Oubliette +Lowland Oaf +Windbrisk Heights +Marsh Gas +Kjeldoran Guard +Island of Wak-Wak +Coal Golem +Balduvian Shaman +Barrenton Medic +Kjeldoran Skyknight +Vampire Lacerator +Leviathan +Forbidden Crypt +Grave Peril +Kjeldoran Skycaptain +Shapeshifter +Barter in Blood +Notorious Throng +Eye for an Eye +Sage of Fables +Elder Spawn +Ragnar +Soldevi Adnate +Aquitect's Will +Prismatic Circle +Seeds of Innocence +Magma Mine +Sealed Fate +Coral Reef +Shimmering Grotto +Spell Blast +Browse +Drop of Honey +Dance of Many +Stampeding Wildebeests +Stone Giant +Innocent Blood +Twiddle +Polymorph +Jester's Cap +Sygg, River Guide +Magma Giant +Soulshriek +Phyrexian Gremlins +Initiates of the Ebon Hand +Takklemaggot +Wild Growth +Marsh Flitter +Karoo +Mesa Pegasus +Ethereal Champion +Windfall +Nature's Wrath +Liliana of the Veil +Jamuraan Lion +Gangrenous Zombies +Rogue Skycaptain +Wand of Denial +Kjeldoran Elite Guard +Feldon's Cane +Echoing Truth +Pox +Kukemssa Serpent +Vision Charm +Phelddagrif +Shield Wall +Dispatch +Gaea's Blessing +Righteousness +Proclamation of Rebirth +Winter Sky +Krovikan Sorcerer +Ley Druid +Veldrane of Sengir +Storm Seeker +Sneak Attack +Savage Twister +Grim Monolith +Hearth Charm +Adarkar Unicorn +Profane Command +Diabolic Vision +Coral Fighters +Malignant Growth +Balduvian Hydra +Pyric Salamander +Brainstorm +Sea Scryer +Runesword +Dark Heart of the Wood +Ebony Charm +Phyrexian Vault +Hope Charm +Mole Worms +Rakalite +Remedy +Nezumi Bone-Reader +Spitting Slug +Giant Caterpillar +War Elephant +Varchild's War-Riders +Prosperity +Coral Atoll +Daze +Jalum Tome +Cuombajj Witches +Cytoplast Root-Kin +Doomgape +Incandescent Soulstoke +Knight of the Mists +Icatian Phalanx +Vaporous Djinn +Red Elemental Blast +Gossamer Chains +Samite Alchemist +Urza's Bauble +Goblin Recruiter +Triassic Egg +Hasran Ogress +Soraya the Falconer +Vesuvan Doppelganger +Basalt Monolith +Armageddon Clock +Anaba Ancestor +Emerald Charm +Everglades +Maro +Clockwork Beast +Soldevi Excavations +Jungle Basin +Zhalfirin Crusader +Vampiric Tutor +Funeral Charm +Vigilant Martyr +Quirion Ranger +Ponder +Tinder Wall +Lotus Cobra +Balm of Restoration +Winds of Change +Guardian Angel +Manamorphose +Sandstorm +Dormant Volcano +Rise of the Hobgoblins +Orcish Cannoneers +Berserk +Bone Harvest +Agent of Stromgald +Time Elemental +Conch Horn +Giant Trap Door Spider +Ghazban Ogre +Phantasmal Terrain +Orcish Artillery +Rainbow Efreet +Hurr Jackal +Familiar's Ruse +Mist Dragon +Clockwork Avian +Serendib Djinn +Sylvan Library +Jandor's Ring +Armor of Thorns +Icatian Infantry +Feast or Famine +Stormbind +Dark Ritual +Searing Spear Askari +Basal Thrull +Ivory Gargoyle +Gemstone Mine +Drain Life +Howl from Beyond +Mystical Tutor +Gorilla Shaman +Triskelion +Mind Twist +Demonic Tutor +Wheel of Fortune +Mana Vault +Force of Will +Snake Basket +Impulse +Hurricane +Fellwar Stone +Dig Through Time +Treasure Cruise +Snapcaster Mage +Enlightened Tutor +Diabolic Tutor +Idyllic Tutor +Personal Tutor +Sylvan Tutor +Worldly Tutor +Vampiric Tutor +Grim Tutor +Merchant Scroll + diff --git a/forge-gui/tools/deckAiCompat.py b/forge-gui/tools/deckAiCompat.py old mode 100644 new mode 100755 index 70b57a32bb9..e7c429f3345 --- a/forge-gui/tools/deckAiCompat.py +++ b/forge-gui/tools/deckAiCompat.py @@ -1,19 +1,23 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 + +CARDSFOLDER = "../../res/cardsfolder" +DECKFOLDER = "." import argparse, os, re -print("Agetian's MTG Forge Deck AI Compatibility Analyzer v1.0\n") +print("Agetian's MTG Forge Deck AI Compatibility Analyzer v4.0\n") parser = argparse.ArgumentParser(description="Analyze MTG Forge decks for AI compatibility.") parser.add_argument("-p", action="store_true", help="print only AI-playable decks") parser.add_argument("-u", action="store_true", help="print only AI-unplayable decks") parser.add_argument("-d", action="store_true", help="physically delete unplayable decks") +parser.add_argument("-s", action="store_true", help="ignore sideboard when judging playability of decks") args = parser.parse_args() # simple structural self-test (can this tool work?) -if not (os.access(os.path.join("cardsfolder","a","abu_jafar.txt"),os.F_OK) or os.access(os.path.join("decks"),os.F_OK)): - print("Fatal error:\n This utility requires the 'cardsfolder' folder with unpacked card files and the 'decks' folder with .dck files in the current directory in order to operate. Exiting.") +if not (os.access(os.path.join(CARDSFOLDER,"a","abu_jafar.txt"),os.F_OK) or os.access(os.path.join("decks"),os.F_OK)): + print("Fatal error:\n This utility requires the 'cardsfolder' folder with unpacked card files at " + CARDSFOLDER + " and the 'decks' folder with .dck files at " + DECKFOLDER + " in order to operate. Exiting.") exit(1) if args.p and args.u: print("Fatal error:\n The -p and -u options are mutually exclusive, please specify one of these options and not both of them at the same time.") @@ -29,28 +33,44 @@ nonplayable_in_deck = 0 # main algorithm print("Loading cards...") -for root, dirs, files in os.walk("cardsfolder"): +for root, dirs, files in os.walk(CARDSFOLDER): for name in files: - if name.find(".txt") != -1: - total_cards += 1 - fullpath = os.path.join(root, name) - cardtext = open(fullpath).read() - cardtext_lower = cardtext.lower() - cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':') - cardname = ":".join(cardname_literal[1:]).strip() - if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1): - # split card, special handling needed - cardsplittext = cardtext.replace('\r','').split('\n') - cardnames = [] - for line in cardsplittext: - if line.lower().find("name:") != -1: - cardnames.extend([line.split('\n')[0].split(':')[1]]) - cardname = " // ".join(cardnames) - if cardtext.lower().find("removedeck:all") != -1: - cardlist[cardname] = 0 - else: - cardlist[cardname] = 1 - ai_playable_cards += 1 + if name.find(".txt") != -1: + total_cards += 1 + fullpath = os.path.join(root, name) + cardtext = open(fullpath).read() + cardtext_lower = cardtext.lower() + cardname_lines = cardtext.replace('\r','').split('\n') + cardname = "" + for line in cardname_lines: + if line.strip().lower().startswith("name:"): + if line.count(':') == 1: + cardname = line.split(':')[1].strip() + break + if cardname == "": + cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':') + cardname = ":".join(cardname_literal[1:]).strip() + if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1): + # split card, special handling needed + cardsplittext = cardtext.replace('\r','').split('\n') + cardnames = [] + for line in cardsplittext: + if line.lower().find("name:") != -1: + cardnames.extend([line.split('\n')[0].split(':')[1]]) + cardname = " // ".join(cardnames) + if (cardtext_lower.find("alternatemode:modal") != -1) or (cardtext_lower.find("alternatemode: modal") != -1): + # ZNR modal card, special handling needed + cardsplittext = cardtext.replace('\r','').split('\n') + cardnames = [] + for line in cardsplittext: + if line.lower().find("name:") != -1: + cardnames.extend([line.split('\n')[0].split(':')[1]]) + cardname = cardnames[0].strip() + if cardtext.lower().find("remaideck") != -1 or cardtext.lower().find("ai:removedeck:all") != -1: + cardlist[cardname] = 0 + else: + cardlist[cardname] = 1 + ai_playable_cards += 1 perc_playable = (float(ai_playable_cards) / total_cards) * 100 perc_unplayable = ((float(total_cards) - ai_playable_cards) / total_cards) * 100 @@ -58,30 +78,34 @@ perc_unplayable = ((float(total_cards) - ai_playable_cards) / total_cards) * 100 print("Loaded %d cards, among them %d playable by the AI (%d%%), %d unplayable by the AI (%d%%).\n" % (total_cards, ai_playable_cards, perc_playable, total_cards - ai_playable_cards, perc_unplayable)) print("Scanning decks...") -for root, dirs, files in os.walk("decks"): +for root, dirs, files in os.walk(DECKFOLDER): for name in files: - if name.find(".dck") != -1: - total_decks += 1 - nonplayable_in_deck = 0 - cardnames = [] - fullpath = os.path.join(root, name) - deckdata = open(fullpath).readlines() - for line in deckdata: - regexobj = re.search('^([0-9]+) +([^|]+)', line) - if regexobj: - cardname = regexobj.groups()[1].replace('\n','').replace('\r','').strip() - if cardlist[cardname] == 0: - cardnames.extend([cardname]) - nonplayable_in_deck += 1 - if nonplayable_in_deck == 0: - if not args.u: - playable_decks += 1 - print("%s is PLAYABLE by the AI." % name) - else: - if not args.p: - print("%s is UNPLAYABLE by the AI (%d unplayable cards: %s)." % (name, nonplayable_in_deck, str(cardnames))) - if args.d: - os.remove(os.path.join(root, name)) + if name.find(".dck") != -1: + total_decks += 1 + nonplayable_in_deck = 0 + cardnames = [] + fullpath = os.path.join(root, name) + deckdata = open(fullpath).readlines() + for line in deckdata: + if args.s: + if line.strip().lower() == "[sideboard]": + break + regexobj = re.search('^([0-9]+) +([^|]+)', line) + if regexobj: + cardname = regexobj.groups()[1].replace('\n','').replace('\r','').strip() + cardname = cardname.replace('\xC6', 'AE') + if cardlist[cardname] == 0: + cardnames.extend([cardname]) + nonplayable_in_deck += 1 + if nonplayable_in_deck == 0: + if not args.u: + playable_decks += 1 + print("%s is PLAYABLE by the AI." % name) + else: + if not args.p: + print("%s is UNPLAYABLE by the AI (%d unplayable cards: %s)." % (name, nonplayable_in_deck, str(cardnames))) + if args.d: + os.remove(os.path.join(root, name)) perc_playable_decks = (float(playable_decks) / total_decks) * 100 perc_unplayable_decks = ((float(total_decks) - playable_decks) / total_decks) * 100 diff --git a/forge-gui/tools/deckAiCompat_limext.py b/forge-gui/tools/deckAiCompat_limext.py new file mode 100755 index 00000000000..4d3a5fd43b7 --- /dev/null +++ b/forge-gui/tools/deckAiCompat_limext.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + +CARDSFOLDER = "../../res/cardsfolder" +DECKFOLDER = "." + +import argparse, os, re + +print("Agetian's MTG Forge Deck AI Compatibility Analyzer v4.0\n") + +parser = argparse.ArgumentParser(description="Analyze MTG Forge decks for AI compatibility.") +parser.add_argument("-p", action="store_true", help="print only AI-playable decks") +parser.add_argument("-u", action="store_true", help="print only AI-unplayable decks") +parser.add_argument("-d", action="store_true", help="physically delete unplayable decks") +parser.add_argument("-s", action="store_true", help="ignore sideboard when judging playability of decks") +parser.add_argument("-l", action="store_true", help="log unsupported cards to ai_unsupported.log") +parser.add_argument("-x", action="store_true", help="account for limited-playable cards from ai_limitedplayable.lst") + +args = parser.parse_args() + +# simple structural self-test (can this tool work?) +if not (os.access(os.path.join(CARDSFOLDER,"a","abu_jafar.txt"),os.F_OK) or os.access(os.path.join("decks"),os.F_OK)): + print("Fatal error:\n This utility requires the 'cardsfolder' folder with unpacked card files at " + CARDSFOLDER + " and the 'decks' folder with .dck files at " + DECKFOLDER + " in order to operate. Exiting.") + exit(1) +if args.p and args.u: + print("Fatal error:\n The -p and -u options are mutually exclusive, please specify one of these options and not both of them at the same time.") + exit(1) + +# basic variables +cardlist = {} +total_cards = 0 +ai_playable_cards = 0 +total_decks = 0 +playable_decks = 0 +nonplayable_in_deck = 0 + +unplayable_cards = {} +limited_playable_cards = [] + +# limited-playable +if args.x: + ff = open("ai_limitedplayable.lst").readlines() + for line in ff: + limited_playable_cards.extend([line.replace("\n","")]) + +# main algorithm +print("Loading cards...") +for root, dirs, files in os.walk(CARDSFOLDER): + for name in files: + if name.find(".txt") != -1: + total_cards += 1 + fullpath = os.path.join(root, name) + cardtext = open(fullpath).read() + cardtext_lower = cardtext.lower() + cardname_lines = cardtext.replace('\r','').split('\n') + cardname = "" + for line in cardname_lines: + if line.strip().lower().startswith("name:"): + if line.count(':') == 1: + cardname = line.split(':')[1].strip() + break + if cardname == "": + cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':') + cardname = ":".join(cardname_literal[1:]).strip() + if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1): + # split card, special handling needed + cardsplittext = cardtext.replace('\r','').split('\n') + cardnames = [] + for line in cardsplittext: + if line.lower().find("name:") != -1: + cardnames.extend([line.split('\n')[0].split(':')[1]]) + cardname = " // ".join(cardnames) + if (cardtext_lower.find("alternatemode:modal") != -1) or (cardtext_lower.find("alternatemode: modal") != -1): + # ZNR modal card, special handling needed + cardsplittext = cardtext.replace('\r','').split('\n') + cardnames = [] + for line in cardsplittext: + if line.lower().find("name:") != -1: + cardnames.extend([line.split('\n')[0].split(':')[1]]) + cardname = cardnames[0].strip() + if cardtext.lower().find("remaideck") != -1 or cardtext.lower().find("ai:removedeck:all") != -1: + cardlist[cardname] = 0 + else: + cardlist[cardname] = 1 + ai_playable_cards += 1 + +perc_playable = (float(ai_playable_cards) / total_cards) * 100 +perc_unplayable = ((float(total_cards) - ai_playable_cards) / total_cards) * 100 + +print("Loaded %d cards, among them %d playable by the AI (%d%%), %d unplayable by the AI (%d%%).\n" % (total_cards, ai_playable_cards, perc_playable, total_cards - ai_playable_cards, perc_unplayable)) + +print("Scanning decks...") +for root, dirs, files in os.walk(DECKFOLDER): + for name in files: + if name.find(".dck") != -1: + total_decks += 1 + nonplayable_in_deck = 0 + cardnames = [] + fullpath = os.path.join(root, name) + deckdata = open(fullpath).readlines() + lim_playable = False + for line in deckdata: + if args.s: + if line.strip().lower() == "[sideboard]": + break + regexobj = re.search('^([0-9]+) +([^|]+)', line) + if regexobj: + cardname = regexobj.groups()[1].replace('\n','').replace('\r','').strip() + cardname = cardname.replace('\xC6', 'AE') + cardname = cardname.replace("AEther Mutation", "Aether Mutation") + cardname = cardname.replace("AEther Membrane", "Aether Membrane") + if cardlist[cardname] == 0: + if limited_playable_cards.count(cardname) > 0: + print("Found limited playable: " + cardname) + lim_playable = True + continue + cardnames.extend([cardname]) + nonplayable_in_deck += 1 + if not cardname in unplayable_cards.keys(): + unplayable_cards[cardname] = 1 + else: + unplayable_cards[cardname] = unplayable_cards[cardname] + 1 + if nonplayable_in_deck == 0: + if not args.u: + playable_decks += 1 + print("%s is PLAYABLE by the AI." % name) + if lim_playable: + os.rename(os.path.join(root, name), os.path.join(root, name.replace(".dck", " [!].dck").replace("[!] [!]", "[!]"))) + else: + if not args.p: + print("%s is UNPLAYABLE by the AI (%d unplayable cards: %s)." % (name, nonplayable_in_deck, str(cardnames))) + if args.d: + os.remove(os.path.join(root, name)) + +perc_playable_decks = (float(playable_decks) / total_decks) * 100 +perc_unplayable_decks = ((float(total_decks) - playable_decks) / total_decks) * 100 + +print("\nScanned %d decks, among them %d playable by the AI (%d%%), %d unplayable by the AI (%d%%)." % (total_decks, playable_decks, perc_playable_decks, total_decks - playable_decks, perc_unplayable_decks)) + +if args.l: + logfile = open("ai_unplayable.log", "w") + sorted_dict = sorted(unplayable_cards, key=unplayable_cards.__getitem__) + for k in sorted_dict: + logfile.write(str(unplayable_cards[k]) + " times: " + k + "\n") + logfile.close() diff --git a/forge-gui/tools/scryfallPricesGenerator.py b/forge-gui/tools/scryfallPricesGenerator.py new file mode 100755 index 00000000000..20f25093d2d --- /dev/null +++ b/forge-gui/tools/scryfallPricesGenerator.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json, os + +# Note: currently only loads the first json file found in the folder! +files = os.listdir(".") +for filename in files: + if filename.endswith(".json"): + metadata_filename = filename + break + +print(f"Loading {metadata_filename}...") +metadata_file = open(metadata_filename, "r") +metadata = json.load(metadata_file) +metadata_file.close() + +prices = {} +art_indexes = {} +always_with_artindex = ["Plains", "Island", "Swamp", "Mountain", "Forest", "Wastes", "Snow-Covered Plains", "Snow-Covered Island", "Snow-Covered Swamp", "Snow-Covered Mountain", "Snow-Covered Forest", "Snow-Covered Wastes"] + +for object in metadata: + obj_type = object["object"] + if obj_type == "card": + card_name = object["name"] + # split cards use //, other cards with two sides (e.g. DFC) use the front face in Forge + if card_name.find("//") != -1 and object["layout"] != "split": + card_name = card_name.split("//")[0].strip() + card_set = object["set"].upper() + card_cn = object["collector_number"] + card_foil_only = object["foil"] and not object["nonfoil"] + card_price = None + if object["prices"]["usd"] == None and object["prices"]["usd_foil"] == None and object["prices"]["usd_etched"] == None and object["prices"]["eur"] == None and object["prices"]["eur_foil"] == None and object["prices"]["tix"] == None: + continue + if not card_foil_only and object["prices"]["usd"] != None: + card_price = object["prices"]["usd"].replace(".", "") + elif object["prices"]["usd_foil"] != None: + card_price = object["prices"]["usd_foil"].replace(".", "") + elif object["prices"]["usd_etched"] != None: + card_price = object["prices"]["usd_etched"].replace(".", "") + elif object["prices"]["eur"] != None: + card_price = object["prices"]["eur"].replace(".", "") + elif object["prices"]["eur_foil"] != None: + card_price = object["prices"]["eur_foil"].replace(".", "") + elif object["prices"]["tix"] != None: + card_price = object["prices"]["tix"].replace(".", "") + if card_price == None: + continue + elif card_price.startswith("00"): + card_price = card_price[2:] + elif card_price.startswith("0"): + card_price = card_price[1:] + # add a key to the prices dictionary, per set + if not card_set in prices: + prices[card_set] = {} + if not card_set in art_indexes: + art_indexes[card_set] = {} + if card_name not in art_indexes[card_set]: + art_indexes[card_set][card_name] = 1 + else: + art_indexes[card_set][card_name] += 1 + if card_name in prices[card_set] or card_name in always_with_artindex: + card_name += f" ({art_indexes[card_set][card_name]})" + prices[card_set][card_name] = card_price + +# Merge with the previous price list if appropriate +if os.path.exists("all-prices.prev"): + print() + merge = input("Would you like to merge the prices with all-prices.prev? ") + if merge.lower().startswith("y"): + prev = open("all-prices.prev", "r") + prev_data = prev.readlines() + prev.close() + for prev_price in prev_data: + if prev_price.find("=") == -1: + continue + data = prev_price.split("=") + old_price = data[1].strip() + old_name = data[0].split("|")[0] + if old_name.find("(") != -1: # unsafe to import a qualified name, we don't know how they'll match up + continue + old_set = data[0].split("|")[1] + if old_set in prices.keys() and old_name not in prices[old_set]: + prices[old_set][old_name] = old_price + +# Generate the prices file +output = open("all-prices.txt", "w") +sorted_prices = {key: value for key, value in sorted(prices.items())} +for set in sorted_prices.keys(): + sorted_cards = {key: value for key, value in sorted(prices[set].items())} + for name in sorted_cards.keys(): + qualified_name = name + if name.find("(") == -1 and name in art_indexes[set] and art_indexes[set][name] > 1: + qualified_name += " (1)" + print(qualified_name + "|" + set + "=" + sorted_cards[name]) + output.write(f"{qualified_name}|{set}={sorted_cards[name]}\n") +output.close()