mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-14 17:58:01 +00:00
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:
407
forge-gui/tools/DeckConversionTools/mtgdecksnet_convert.py
Normal file → Executable file
407
forge-gui/tools/DeckConversionTools/mtgdecksnet_convert.py
Normal file → Executable 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")
|
||||||
|
|||||||
236
forge-gui/tools/DeckConversionTools/mtgdecksnet_decksort.py
Normal file → Executable file
236
forge-gui/tools/DeckConversionTools/mtgdecksnet_decksort.py
Normal file → Executable 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)
|
||||||
|
|
||||||
|
|||||||
36
forge-gui/tools/DeckConversionTools/mtgdecksnet_decksort_EDH.py
Normal file → Executable file
36
forge-gui/tools/DeckConversionTools/mtgdecksnet_decksort_EDH.py
Normal file → Executable 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))
|
||||||
|
|||||||
572
forge-gui/tools/ai_limitedplayable.lst
Normal file
572
forge-gui/tools/ai_limitedplayable.lst
Normal 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
120
forge-gui/tools/deckAiCompat.py
Normal file → Executable 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
|
||||||
|
|||||||
144
forge-gui/tools/deckAiCompat_limext.py
Executable file
144
forge-gui/tools/deckAiCompat_limext.py
Executable 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()
|
||||||
97
forge-gui/tools/scryfallPricesGenerator.py
Executable file
97
forge-gui/tools/scryfallPricesGenerator.py
Executable 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()
|
||||||
Reference in New Issue
Block a user