Update the MTGDecksNet conversion and AI playability test toolchain to the latest version (#2363)

* - Update the MTGDecksNet conversion and AI playability test toolchain to the latest version (Python 3 compatible).

* - Make the input/output folders generic

* - Make the input/output folders generic, part 2.

* - Add the Scryfall all-prices.txt generator script.

* - Minor code cleanup.
This commit is contained in:
Agetian
2023-02-02 07:51:45 +03:00
committed by GitHub
parent 28166530ea
commit 10c6d71140
7 changed files with 1255 additions and 357 deletions

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Modify key directories here # Modify key directories here
@@ -9,7 +9,7 @@ OUT_DECKFOLDER = "./ForgeDecks"
import argparse, os, re 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.") parser = argparse.ArgumentParser(description="Convert MtgDecks.net DEC to Forge DCK.")
@@ -40,34 +40,51 @@ nonplayable_in_deck = 0
print("Loading cards...") print("Loading cards...")
for root, dirs, files in os.walk(CARDSFOLDER): for root, dirs, files in os.walk(CARDSFOLDER):
for name in files: for name in files:
if name.find(".txt") != -1: if name.find(".txt") != -1:
total_cards += 1 total_cards += 1
fullpath = os.path.join(root, name) fullpath = os.path.join(root, name)
cardtext = open(fullpath).read() cardtext = open(fullpath).read()
cardtext_lower = cardtext.lower() cardtext_lower = cardtext.lower()
cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':') cardname_lines = cardtext.replace('\r','').split('\n')
cardname = ":".join(cardname_literal[1:]).strip() cardname = ""
if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1): for line in cardname_lines:
# split card, special handling needed if line.strip().lower().startswith("name:"):
cardsplittext = cardtext.replace('\r','').split('\n') if line.count(':') == 1:
cardnames = [] cardname = line.split(':')[1].strip()
for line in cardsplittext: break
if line.lower().find("name:") != -1: if cardname == "":
cardnames.extend([line.split('\n')[0].split(':')[1]]) cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':')
cardname = " // ".join(cardnames) cardname = ":".join(cardname_literal[1:]).strip()
if cardtext.lower().find("removedeck:all") != -1: if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1):
cardlist[cardname] = 0 # split card, special handling needed
else: cardsplittext = cardtext.replace('\r','').split('\n')
cardlist[cardname] = 1 cardnames = []
ai_playable_cards += 1 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_playable = (float(ai_playable_cards) / total_cards) * 100
perc_unplayable = ((float(total_cards) - 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("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_Metadata2 = '^//(.*) a ([A-Za-z]+) MTG deck played by (.*) in (.*) - MTGDECKS.NET.*$'
re_Metadata3 = '^//(.*) a (.*) deck by (.*)$'
re_DeckID = '^([0-9]+)\.dec$' re_DeckID = '^([0-9]+)\.dec$'
re_Maindeck = '^([0-9]+) (.*)$' re_Maindeck = '^([0-9]+) (.*)$'
re_Sideboard = '^SB:[ \t]+([0-9]+) (.*)$' re_Sideboard = '^SB:[ \t]+([0-9]+) (.*)$'
@@ -79,173 +96,193 @@ badChars = ['/', '\\', '*']
print("Converting decks...") print("Converting decks...")
for root, dirs, files in os.walk(DECKFOLDER): for root, dirs, files in os.walk(DECKFOLDER):
for name in files: for name in files:
if name.find(".dec") != -1: if name.find(".dec") != -1:
print("Converting deck: " + name + "...") print("Converting deck: " + name + "...")
deck_id = -1 deck_id = -1
s_DeckID = re.search(re_DeckID, name) s_DeckID = re.search(re_DeckID, name)
if s_DeckID: if s_DeckID:
deck_id = s_DeckID.groups()[0] deck_id = s_DeckID.groups()[0]
fullpath = os.path.join(root, name) fullpath = os.path.join(root, name)
deckdata = open(fullpath).readlines() deckdata = open(fullpath).readlines()
name = "" name = ""
creator = "" creator = ""
format = "" format = ""
maindeck = [] maindeck = []
maindeck_cards = 0 maindeck_cards = 0
sideboard = [] sideboard = []
supported = True supported = True
deckHasUnsupportedCards = False deckHasUnsupportedCards = False
for line in deckdata: for line in deckdata:
#line = line.replace("\xE1", "a") #line = line.replace("\xE1", "a")
#line = line.replace("\xFB", "u") #line = line.replace("\xFB", "u")
#line = line.replace("\xE9", "e") #line = line.replace("\xE9", "e")
line = line.replace("\xC3\x86", "AE") line = line.replace("\xC3\x86", "AE")
line = line.replace("\xC3\xA9", "e") line = line.replace("\xC3\xA9", "e")
line = line.replace("\xC3\xBB", "u") line = line.replace("\xC3\xBB", "u")
line = line.replace("\xC3\xA1", "a") line = line.replace("\xC3\xA1", "a")
line = line.replace("\xC3\xAD", "i") line = line.replace("\xC3\xAD", "i")
#line = line.replace("Unravel the Aether", "Unravel the AEther") #line = line.replace("Unravel the Aether", "Unravel the AEther")
#line = line.replace("Aether", "AEther") #line = line.replace("Aether", "AEther")
line = line.replace("Chandra, Roaring Flame", "Chandra, Fire of Kaladesh") line = line.replace("Chandra, Roaring Flame", "Chandra, Fire of Kaladesh")
line = line.replace("Nissa, Sage Animist", "Nissa, Vastwood Seer") line = line.replace("Lurrus of the Dream Den", "Lurrus of the Dream-Den")
line = line.replace("Neck Breaker", "Breakneck Rider") line = line.replace("Nissa, Sage Animist", "Nissa, Vastwood Seer")
line = line.replace("\xC3\xB6", "o") line = line.replace("Neck Breaker", "Breakneck Rider")
line = line.replace("\x97", "-") line = line.replace("Avacyn, the Purifier", "Archangel Avacyn")
line = line.replace("\x91", "'") line = line.replace("Dandân", "Dandan")
line = line.replace("\xFB", "u") line = line.replace("Séance", "Seance")
line = line.replace("\xFC", "u") line = line.replace("Jötun Grunt", "Jotun Grunt")
line = line.replace("\xC4", "A") line = line.replace("Ifh-Bíff Efreet", "Ifh-Biff Efreet")
if line[0] != "/" and line.find(" // ") == -1: line = line.replace("Juzám Djinn", "Juzam Djinn")
line = line.replace("/"," // ") line = line.replace("\xC3\xB6", "o")
timepos = line.find("<!") line = line.replace("\x97", "-")
if timepos > -1: line = line.replace("\x91", "'")
line = line[0:timepos] line = line.replace("\xFB", "u")
isCardSupported = True line = line.replace("\xFC", "u")
mode = 0 line = line.replace("\xC4", "A")
s_Metadata = re.search(re_Metadata, line) if line[0] != "/" and line.find(" // ") == -1:
if not s_Metadata: line = line.replace("/"," // ")
s_Metadata = re.search(re_Metadata2, line) timepos = line.find("<!")
mode = 1 if timepos > -1:
if s_Metadata: line = line[0:timepos]
name = s_Metadata.groups()[0].strip() isCardSupported = True
format = s_Metadata.groups()[1].strip() mode = 0
creator = s_Metadata.groups()[2].strip() s_Metadata = re.search(re_Metadata, line)
event = "" if not s_Metadata:
if mode == 1: s_Metadata = re.search(re_Metadata2, line)
event = s_Metadata.groups()[3].strip() mode = 1
for badChar in badChars: if not s_Metadata:
event = event.replace(badChar, "-") s_Metadata = re.search(re_Metadata3, line)
if args.t and args.t != format: mode = 0
print("Skipping an off-format deck " + name + " (format = " + format + ")") if s_Metadata:
supported = False name = s_Metadata.groups()[0].strip()
continue format = s_Metadata.groups()[1].strip()
s_Maindeck = re.search(re_Maindeck, line) creator = s_Metadata.groups()[2].strip()
if s_Maindeck: event = ""
cardAmount = s_Maindeck.groups()[0].strip() if mode == 1:
cardName = s_Maindeck.groups()[1].strip() event = s_Metadata.groups()[3].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")): for badChar in badChars:
print("Unsupported card (MAIN): " + cardName) event = event.replace(badChar, "-")
if args.f: if args.t and args.t != format:
supported = False print("Skipping an off-format deck " + name + " (format = " + format + ")")
else: supported = False
isCardSupported = False continue
deckHasUnsupportedCards = True s_Maindeck = re.search(re_Maindeck, line)
if not cardName in unsupportedList: if s_Maindeck:
unsupportedList.extend([cardName]) cardAmount = s_Maindeck.groups()[0].strip()
if cardlist.has_key(cardName): cardName = s_Maindeck.groups()[1].strip()
mdline = cardAmount + " " + cardName if cardName == "":
elif cardlist.has_key(cardName.replace("Aether", "AEther")): continue
mdline = cardAmount + " " + cardName.replace("Aether", "AEther") altModalKey = cardName.split(" // ")[0].strip()
elif cardlist.has_key(cardName.replace("AEther", "Aether")): 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():
mdline = cardAmount + " " + cardName.replace("AEther", "Aether") print("Unsupported card (MAIN): " + cardName)
else: if args.f:
mdline = cardAmount + " " + cardName # for the purposes of unsupported cards supported = False
if isCardSupported: else:
maindeck.extend([mdline]) isCardSupported = False
else: deckHasUnsupportedCards = True
maindeck.extend(["#"+mdline]) if not cardName in unsupportedList:
maindeck_cards += int(cardAmount) unsupportedList.extend([cardName])
continue if altModalKey in cardlist.keys():
s_Sideboard = re.search(re_Sideboard, line) mdline = cardAmount + " " + altModalKey # ZNR modal cards with //
if s_Sideboard: elif cardName in cardlist.keys():
cardAmount = s_Sideboard.groups()[0].strip() mdline = cardAmount + " " + cardName
cardName = s_Sideboard.groups()[1].strip() elif cardName.replace("Aether", "AEther") in cardlist.keys():
if not cardlist.has_key(cardName) and not cardlist.has_key(cardName.replace("Aether", "AEther")) and not cardlist.has_key(cardName.replace("AEther", "Aether")): mdline = cardAmount + " " + cardName.replace("Aether", "AEther")
print("Unsupported card (SIDE): " + cardName) elif cardName.replace("AEther", "Aether") in cardlist.keys():
if args.f: mdline = cardAmount + " " + cardName.replace("AEther", "Aether")
supported = False else:
else: mdline = cardAmount + " " + cardName # for the purposes of unsupported cards
isCardSupported = False if isCardSupported:
deckHasUnsupportedCards = True maindeck.extend([mdline])
if not cardName in unsupportedList: else:
unsupportedList.extend([cardName]) maindeck.extend(["#"+mdline])
if cardlist.has_key(cardName): maindeck_cards += int(cardAmount)
sdline = cardAmount + " " + cardName continue
elif cardlist.has_key(cardName.replace("Aether", "AEther")): s_Sideboard = re.search(re_Sideboard, line)
sdline = cardAmount + " " + cardName.replace("Aether", "AEther") if s_Sideboard:
elif cardlist.has_key(cardName.replace("AEther", "Aether")): cardAmount = s_Sideboard.groups()[0].strip()
sdline = cardAmount + " " + cardName.replace("AEther", "Aether") cardName = s_Sideboard.groups()[1].strip()
else: if cardName == "":
sdline = cardAmount + " " + cardName # for the purposes of unsupported cards continue
if isCardSupported: altModalKey = cardName.split(" // ")[0].strip()
sideboard.extend([sdline]) 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():
else: print("Unsupported card (SIDE): " + cardName)
sideboard.extend(["#"+sdline]) if args.f:
continue 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 # convert here
if supported and len(maindeck) > 0: if supported and len(maindeck) > 0:
if creator != "": if creator != "":
deckname = creator + " - " + name + " (" deckname = creator + " - " + name + " ("
else: else:
deckname = name + " (" deckname = name + " ("
deckname += format deckname += format
if args.i and deck_id > -1: if args.i and int(deck_id) > -1:
if format != "": if format != "":
deckname += ", #" + deck_id deckname += ", #" + deck_id
else: else:
deckname += "#" + deck_id deckname += "#" + deck_id
deckname += ")" deckname += ")"
#deckname = (c for c in deckname if ord(c) < 128) #deckname = (c for c in deckname if ord(c) < 128)
if not args.U: if not args.U:
deckname = re.sub(r'[^\x00-\x7F]', '@', deckname) deckname = re.sub(r'[^\x00-\x7F]', '@', deckname)
deckname = re.sub(r'[/\\]', '-', deckname) deckname = re.sub(r'[/\\]', '-', deckname)
deckname = re.sub(r'[?*]', '_', deckname) deckname = re.sub(r'[?*]', '_', deckname)
if args.w and deckHasUnsupportedCards: if args.w and deckHasUnsupportedCards:
deckname += " [!]" deckname += " [!]"
if args.D: if args.D:
if deckHasUnsupportedCards: if deckHasUnsupportedCards:
outname = "Unsupported" + "/" + format + "/" outname = "Unsupported" + "/" + format + "/"
pathToUnsupported = "./" + OUT_DECKFOLDER + "/Unsupported/" + format pathToUnsupported = "./" + OUT_DECKFOLDER + "/Unsupported/" + format
if not os.path.isdir(pathToUnsupported): if not os.path.isdir(pathToUnsupported):
os.makedirs(pathToUnsupported) os.makedirs(pathToUnsupported)
else: else:
outname = format + "/" outname = format + "/"
if not os.path.isdir("./" + OUT_DECKFOLDER + "/" + format): if not os.path.isdir("./" + OUT_DECKFOLDER + "/" + format):
os.makedirs("./" + OUT_DECKFOLDER + "/" + format) os.makedirs("./" + OUT_DECKFOLDER + "/" + format)
else: else:
outname = "" outname = ""
outname += deckname + ".dck" outname += deckname + ".dck"
print ("Writing converted deck: " + outname) print ("Writing converted deck: " + outname)
dck = open(OUT_DECKFOLDER + "/" + outname, "w") dck = open(OUT_DECKFOLDER + "/" + outname, "w")
if event: if event:
dck.write("#EVENT:"+event+"\n") dck.write("#EVENT:"+event+"\n")
dck.write("[metadata]\n") dck.write("[metadata]\n")
dck.write("Name="+deckname+"\n") dck.write("Name="+deckname+"\n")
dck.write("[general]\n") dck.write("[general]\n")
dck.write("Constructed\n") dck.write("Constructed\n")
dck.write("[Main]\n") dck.write("[Main]\n")
for m in maindeck: for m in maindeck:
dck.write(m+"\n") dck.write(m+"\n")
if not (format == "Commander" and len(sideboard) == 1): 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") dck.write("[Sideboard]\n")
else: else:
dck.write("[Commander]\n") dck.write("[Commander]\n")
for s in sideboard: for s in sideboard:
dck.write(s+"\n") dck.write(s+"\n")
# write out unsupported cards # write out unsupported cards
log = open("dec2forge.log", "w") log = open("dec2forge.log", "w")

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
CARDSFOLDER = "../../res/cardsfolder" CARDSFOLDER = "../../res/cardsfolder"
EDITIONS = "../../res/editions" EDITIONS = "../../res/editions"
@@ -6,10 +6,11 @@ DECKFOLDER = "."
import argparse, os, re, shutil 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 = 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("-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() args = parser.parse_args()
@@ -44,26 +45,34 @@ re_Card = '^[0-9]* *[A-Z] (.*)$'
print("Loading cards...") print("Loading cards...")
for root, dirs, files in os.walk(CARDSFOLDER): for root, dirs, files in os.walk(CARDSFOLDER):
for name in files: for name in files:
if name.find(".txt") != -1: if name.find(".txt") != -1:
total_cards += 1 total_cards += 1
fullpath = os.path.join(root, name) fullpath = os.path.join(root, name)
cardtext = open(fullpath).read() cardtext = open(fullpath).read()
cardtext_lower = cardtext.lower() cardtext_lower = cardtext.lower()
cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':') cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':')
cardname = ":".join(cardname_literal[1:]).strip() cardname = ":".join(cardname_literal[1:]).strip()
if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1): if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1):
# split card, special handling needed # split card, special handling needed
cardsplittext = cardtext.replace('\r','').split('\n') cardsplittext = cardtext.replace('\r','').split('\n')
cardnames = [] cardnames = []
for line in cardsplittext: for line in cardsplittext:
if line.lower().find("name:") != -1: if line.lower().find("name:") != -1:
cardnames.extend([line.split('\n')[0].split(':')[1]]) cardnames.extend([line.split('\n')[0].split(':')[1]])
cardname = " // ".join(cardnames) cardname = " // ".join(cardnames)
if cardtext.lower().find("removedeck:all") != -1: if (cardtext_lower.find("alternatemode:modal") != -1) or (cardtext_lower.find("alternatemode: modal") != -1):
cardlist[cardname] = 0 # ZNR modal card, special handling needed
else: cardsplittext = cardtext.replace('\r','').split('\n')
cardlist[cardname] = 1 cardnames = []
ai_playable_cards += 1 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_playable = (float(ai_playable_cards) / total_cards) * 100
perc_unplayable = ((float(total_cards) - 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...") print("Loading editions...")
for root, dirs, files in os.walk(EDITIONS): for root, dirs, files in os.walk(EDITIONS):
for name in files: for name in files:
if name.find(".txt") != -1: if name.find(".txt") != -1:
total_editions += 1 total_editions += 1
fullpath = os.path.join(root, name) fullpath = os.path.join(root, name)
edition = open(fullpath).readlines() edition = open(fullpath).readlines()
foundCards = False foundCards = False
code = "" code = ""
date = "" date = ""
etype = "" etype = ""
name = "" name = ""
for line in edition: for line in edition:
line = line.replace("\r\n","") line = line.replace("\r\n","")
if not foundCards: line = line.split(" @")[0]
if line == "[cards]": if not foundCards:
foundCards = True if line.find("[cards]") != -1:
else: foundCards = True
s_Code = re.search(re_Code, line) else:
if s_Code: s_Code = re.search(re_Code, line)
code = s_Code.groups()[0] if s_Code:
#print("Code found: " + code) code = s_Code.groups()[0]
s_Date = re.search(re_Date, line) #print("Code found: " + code)
if s_Date: s_Date = re.search(re_Date, line)
date = s_Date.groups()[0] if s_Date:
#print("Date found: " + date) date = s_Date.groups()[0]
s_Date2 = re.search(re_Date2, line) #print("Date found: " + date)
if s_Date2: s_Date2 = re.search(re_Date2, line)
date = s_Date2.groups()[0] + "-01" if s_Date2:
#print("Date found: " + date) date = s_Date2.groups()[0] + "-01"
s_Type = re.search(re_Type, line) #print("Date found: " + date)
if s_Type: s_Type = re.search(re_Type, line)
etype = s_Type.groups()[0] if s_Type:
#print("Type found: " + etype) etype = s_Type.groups()[0]
s_Name = re.search(re_Name, line) #print("Type found: " + etype)
if s_Name: s_Name = re.search(re_Name, line)
name = s_Name.groups()[0] if s_Name:
#print("Name found: " + name) name = s_Name.groups()[0]
else: #print("Name found: " + name)
if etype != "Expansion" and etype != "Core" and etype != "Starter": else:
#print("NOT LOADING: " + code) if etype != "Expansion" and etype != "Core" and etype != "Starter" and code != "VOC" and code != "MIC" and code != "AFC":
continue #print("NOT LOADING: " + code)
else: continue
if editions.keys().count(code) == 0: else:
editions[code] = date if not code in editions.keys():
edition_names[code] = name editions[code] = date
#print(editions) edition_names[code] = name
s_Card = re.search(re_Card, line) #print(editions)
if s_Card: s_Card = re.search(re_Card, line)
card = s_Card.groups()[0].strip() if s_Card:
#print("Card found: " + card) card = s_Card.groups()[0].strip()
if cards_by_edition.keys().count(card) == 0: #print("Card found: " + card)
cards_by_edition[card] = [] if not card in cards_by_edition.keys():
cards_by_edition[card].append(code) cards_by_edition[card] = []
cards_by_edition[card].append(code)
print("Loaded " + str(len(editions)) + " editions.") print("Loaded " + str(len(editions)) + " editions.")
@@ -132,47 +142,47 @@ def get_latest_set_for_card(card):
cdate = "0000-00-00" cdate = "0000-00-00"
edition = "XXX" edition = "XXX"
if ignore_cards.count(card) != 0: if ignore_cards.count(card) != 0:
return "LEA" 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) #print("Warning: couldn't determine an edition for card: " + card)
return "LEA" return "LEA"
for code in cards_by_edition[card]: for code in cards_by_edition[card]:
if editions[code] > cdate: if editions[code] > cdate:
cdate = editions[code] cdate = editions[code]
edition = code edition = code
return edition return edition
def get_earliest_set_for_card(card): def get_earliest_set_for_card(card):
cdate = "9999-99-99" cdate = "9999-99-99"
edition = "XXX" 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) #print("Warning: couldn't determine an edition for card: " + card)
return "LEA" return "LEA"
for code in cards_by_edition[card]: for code in cards_by_edition[card]:
if editions[code] < cdate: if editions[code] < cdate:
cdate = editions[code] cdate = editions[code]
edition = code edition = code
return edition return edition
def get_latest_set_for_deck(deck): def get_latest_set_for_deck(deck):
edition = "LEA" edition = "LEA"
for line in deck.split('\n'): for line in deck.split('\n'):
#print("Line: " + line) #print("Line: " + line)
regexobj = re.search('^([0-9]+) +([^|]+)', line) regexobj = re.search('^([0-9]+) +([^|]+)', line)
if regexobj: if regexobj:
cardname = regexobj.groups()[1].replace('\n','').replace('\r','').strip() cardname = regexobj.groups()[1].replace('\n','').replace('\r','').strip()
earliest_for_card = get_earliest_set_for_card(cardname) earliest_for_card = get_earliest_set_for_card(cardname)
#print("Card: " + cardname + ", latest for it: " + latest_for_card) #print("Card: " + cardname + ", latest for it: " + latest_for_card)
if editions[earliest_for_card] > editions[edition]: if editions[earliest_for_card] > editions[edition]:
edition = earliest_for_card edition = earliest_for_card
return edition return edition
def get_event_for_deck(deck): def get_event_for_deck(deck):
evline = deck.split('\n')[0].split("#EVENT:") evline = deck.split('\n')[0].split("#EVENT:")
if len(evline) == 2: if len(evline) == 2:
return evline[1] return evline[1]
else: else:
return "" return ""
#print(cards_by_edition["Fireball"]) #print(cards_by_edition["Fireball"])
#print(edition_names[get_latest_set_for_card("Fireball")]) #print(edition_names[get_latest_set_for_card("Fireball")])
@@ -189,18 +199,20 @@ def get_event_for_deck(deck):
print("Scanning decks...") print("Scanning decks...")
for root, dirs, files in os.walk(DECKFOLDER): for root, dirs, files in os.walk(DECKFOLDER):
for name in files: for name in files:
if name.find(".dck") != -1: if name.find(".dck") != -1:
total_decks += 1 total_decks += 1
fullpath = os.path.join(root, name) fullpath = os.path.join(root, name)
deckdata = open(fullpath).read() deckdata = open(fullpath).read()
set_for_deck = edition_names[get_latest_set_for_deck(deckdata)] set_for_deck = edition_names[get_latest_set_for_deck(deckdata)]
event_for_deck = get_event_for_deck(deckdata) event_for_deck = get_event_for_deck(deckdata)
if event_for_deck != "" and event_for_deck[len(event_for_deck)-1] == ".": if args.x:
event_for_deck = event_for_deck[0:len(event_for_deck)-1] event_for_deck = ""
print("Deck: " + name + ", Set: " + set_for_deck + ", Event: " + event_for_deck) if event_for_deck != "" and event_for_deck[len(event_for_deck)-1] == ".":
if not os.access(os.path.join(root, set_for_deck, event_for_deck), os.F_OK): event_for_deck = event_for_deck[0:len(event_for_deck)-1]
os.makedirs(os.path.join(root, set_for_deck, event_for_deck)) print("Deck: " + name + ", Set: " + set_for_deck + ", Event: " + event_for_deck)
shutil.copy(fullpath, os.path.join(root, set_for_deck, event_for_deck, name)) if not os.access(os.path.join(root, set_for_deck, event_for_deck), os.F_OK):
if args.d: os.makedirs(os.path.join(root, set_for_deck, event_for_deck))
os.remove(fullpath) shutil.copy(fullpath, os.path.join(root, set_for_deck, event_for_deck, name))
if args.d:
os.remove(fullpath)

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
CARDSFOLDER = "../../res/cardsfolder" CARDSFOLDER = "../../res/cardsfolder"
EDITIONS = "../../res/editions" EDITIONS = "../../res/editions"
@@ -6,10 +6,11 @@ DECKFOLDER = "."
import argparse, os, re, shutil 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 = 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("-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() args = parser.parse_args()
@@ -59,7 +60,15 @@ for root, dirs, files in os.walk(CARDSFOLDER):
if line.lower().find("name:") != -1: if line.lower().find("name:") != -1:
cardnames.extend([line.split('\n')[0].split(':')[1]]) cardnames.extend([line.split('\n')[0].split(':')[1]])
cardname = " // ".join(cardnames) 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 cardlist[cardname] = 0
else: else:
cardlist[cardname] = 1 cardlist[cardname] = 1
@@ -84,8 +93,9 @@ for root, dirs, files in os.walk(EDITIONS):
name = "" name = ""
for line in edition: for line in edition:
line = line.replace("\r\n","") line = line.replace("\r\n","")
line = line.split(" @")[0]
if not foundCards: if not foundCards:
if line == "[cards]": if line.find("[cards]") != -1:
foundCards = True foundCards = True
else: else:
s_Code = re.search(re_Code, line) s_Code = re.search(re_Code, line)
@@ -109,14 +119,14 @@ for root, dirs, files in os.walk(EDITIONS):
name = s_Name.groups()[0] name = s_Name.groups()[0]
#print("Name found: " + name) #print("Name found: " + name)
else: 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) #print("NOT LOADING: " + code)
continue continue
if code == "EXP" or code == "MPS": if code == "EXP" or code == "MPS":
#print("NOT LOADING: " + code) #print("NOT LOADING: " + code)
continue continue
else: else:
if editions.keys().count(code) == 0: if not code in editions.keys():
editions[code] = date editions[code] = date
edition_names[code] = name edition_names[code] = name
#print(editions) #print(editions)
@@ -124,7 +134,7 @@ for root, dirs, files in os.walk(EDITIONS):
if s_Card: if s_Card:
card = s_Card.groups()[0].strip() card = s_Card.groups()[0].strip()
#print("Card found: " + card) #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] = []
cards_by_edition[card].append(code) cards_by_edition[card].append(code)
@@ -136,7 +146,7 @@ def get_latest_set_for_card(card):
edition = "XXX" edition = "XXX"
if ignore_cards.count(card) != 0: if ignore_cards.count(card) != 0:
return "LEA" 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) #print("Warning: couldn't determine an edition for card: " + card)
return "LEA" return "LEA"
for code in cards_by_edition[card]: 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): def get_earliest_set_for_card(card):
cdate = "9999-99-99" cdate = "9999-99-99"
edition = "XXX" 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) #print("Warning: couldn't determine an edition for card: " + card)
return "LEA" return "LEA"
for code in cards_by_edition[card]: for code in cards_by_edition[card]:
@@ -198,8 +208,10 @@ for root, dirs, files in os.walk(DECKFOLDER):
deckdata = open(fullpath).read() deckdata = open(fullpath).read()
set_for_deck = edition_names[get_latest_set_for_deck(deckdata)] set_for_deck = edition_names[get_latest_set_for_deck(deckdata)]
event_for_deck = get_event_for_deck(deckdata) event_for_deck = get_event_for_deck(deckdata)
if event_for_deck != "" and event_for_deck[len(event_for_deck)-1] == ".": if args.x:
event_for_deck = event_for_deck[0:len(event_for_deck)-1] 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) 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): 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)) os.makedirs(os.path.join(root, set_for_deck, event_for_deck))

View File

@@ -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

120
forge-gui/tools/deckAiCompat.py Normal file → Executable file
View File

@@ -1,19 +1,23 @@
#!/usr/bin/env python #!/usr/bin/env python3
CARDSFOLDER = "../../res/cardsfolder"
DECKFOLDER = "."
import argparse, os, re 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 = 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("-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("-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("-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() args = parser.parse_args()
# simple structural self-test (can this tool work?) # 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)): 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.") 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) exit(1)
if args.p and args.u: 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.") 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 # main algorithm
print("Loading cards...") print("Loading cards...")
for root, dirs, files in os.walk("cardsfolder"): for root, dirs, files in os.walk(CARDSFOLDER):
for name in files: for name in files:
if name.find(".txt") != -1: if name.find(".txt") != -1:
total_cards += 1 total_cards += 1
fullpath = os.path.join(root, name) fullpath = os.path.join(root, name)
cardtext = open(fullpath).read() cardtext = open(fullpath).read()
cardtext_lower = cardtext.lower() cardtext_lower = cardtext.lower()
cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':') cardname_lines = cardtext.replace('\r','').split('\n')
cardname = ":".join(cardname_literal[1:]).strip() cardname = ""
if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1): for line in cardname_lines:
# split card, special handling needed if line.strip().lower().startswith("name:"):
cardsplittext = cardtext.replace('\r','').split('\n') if line.count(':') == 1:
cardnames = [] cardname = line.split(':')[1].strip()
for line in cardsplittext: break
if line.lower().find("name:") != -1: if cardname == "":
cardnames.extend([line.split('\n')[0].split(':')[1]]) cardname_literal = cardtext.replace('\r','').split('\n')[0].split(':')
cardname = " // ".join(cardnames) cardname = ":".join(cardname_literal[1:]).strip()
if cardtext.lower().find("removedeck:all") != -1: if (cardtext_lower.find("alternatemode:split") != -1) or (cardtext_lower.find("alternatemode: split") != -1):
cardlist[cardname] = 0 # split card, special handling needed
else: cardsplittext = cardtext.replace('\r','').split('\n')
cardlist[cardname] = 1 cardnames = []
ai_playable_cards += 1 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_playable = (float(ai_playable_cards) / total_cards) * 100
perc_unplayable = ((float(total_cards) - 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("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...") print("Scanning decks...")
for root, dirs, files in os.walk("decks"): for root, dirs, files in os.walk(DECKFOLDER):
for name in files: for name in files:
if name.find(".dck") != -1: if name.find(".dck") != -1:
total_decks += 1 total_decks += 1
nonplayable_in_deck = 0 nonplayable_in_deck = 0
cardnames = [] cardnames = []
fullpath = os.path.join(root, name) fullpath = os.path.join(root, name)
deckdata = open(fullpath).readlines() deckdata = open(fullpath).readlines()
for line in deckdata: for line in deckdata:
regexobj = re.search('^([0-9]+) +([^|]+)', line) if args.s:
if regexobj: if line.strip().lower() == "[sideboard]":
cardname = regexobj.groups()[1].replace('\n','').replace('\r','').strip() break
if cardlist[cardname] == 0: regexobj = re.search('^([0-9]+) +([^|]+)', line)
cardnames.extend([cardname]) if regexobj:
nonplayable_in_deck += 1 cardname = regexobj.groups()[1].replace('\n','').replace('\r','').strip()
if nonplayable_in_deck == 0: cardname = cardname.replace('\xC6', 'AE')
if not args.u: if cardlist[cardname] == 0:
playable_decks += 1 cardnames.extend([cardname])
print("%s is PLAYABLE by the AI." % name) nonplayable_in_deck += 1
else: if nonplayable_in_deck == 0:
if not args.p: if not args.u:
print("%s is UNPLAYABLE by the AI (%d unplayable cards: %s)." % (name, nonplayable_in_deck, str(cardnames))) playable_decks += 1
if args.d: print("%s is PLAYABLE by the AI." % name)
os.remove(os.path.join(root, 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_playable_decks = (float(playable_decks) / total_decks) * 100
perc_unplayable_decks = ((float(total_decks) - playable_decks) / total_decks) * 100 perc_unplayable_decks = ((float(total_decks) - playable_decks) / total_decks) * 100

View File

@@ -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()

View File

@@ -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()