Merge remote-tracking branch 'refs/remotes/billzorn/master'

This commit is contained in:
reimannsum 2016-06-11 13:05:51 -04:00
commit 9b1e607330
29 changed files with 50350 additions and 44332 deletions

View file

@ -4,7 +4,7 @@ Utilities to assist in the process of generating Magic the Gathering cards with
http://www.mtgsalvation.com/forums/creativity/custom-card-creation/612057-generating-magic-cards-using-deep-recurrent-neural http://www.mtgsalvation.com/forums/creativity/custom-card-creation/612057-generating-magic-cards-using-deep-recurrent-neural
The purpose of this code is mostly to wrangle text between various human and machine readable formats. The original input comes from [mtgjson](http://mtgjson.com); this is filtered and reduced to one of several input formats intended for neural network training, such as the standard encoded format used in [data/output.txt](https://github.com/billzorn/mtgencode/blob/master/data/output.txt). Any json or encoded data, including output from appropriately trained neural nets, can then be interpreted as cards and decoded to a human readable format, such as a text spoiler or [Magic Set Editor 2](http://magicseteditor.sourceforge.net) set file. The purpose of this code is mostly to wrangle text between various human and machine readable formats. The original input comes from [mtgjson](http://mtgjson.com); this is filtered and reduced to one of several input formats intended for neural network training, such as the standard encoded format used in [data/output.txt](https://github.com/billzorn/mtgencode/blob/master/data/output.txt). Any json or encoded data, including output from appropriately trained neural nets, can then be interpreted as cards and decoded to a human readable format, such as a text spoiler, [Magic Set Editor 2](http://magicseteditor.sourceforge.net) set file, or a pretty, portable html file that can be viewed in any browser.
## Requirements ## Requirements
@ -21,7 +21,8 @@ Functionality is provided by two main driver scripts: encode.py and decode.py. L
### encode.py ### encode.py
``` ```
usage: encode.py [-h] [-d N] [-e {std,rmana,rmana_dual,rfields,vec}] [-s] [-v] usage: encode.py [-h] [-e {std,named,noname,rfields,old,norarity,vec,custom}]
[-r] [--nolinetrans] [--nolabel] [-s] [-v]
infile [outfile] infile [outfile]
positional arguments: positional arguments:
@ -30,27 +31,33 @@ positional arguments:
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-d N, --duplicate N number of times to duplicate each card -e {std,named,noname,rfields,old,norarity,vec,custom}, --encoding {std,named,noname,rfields,old,norarity,vec,custom}
-e {std,rmana,rmana_dual,rfields,vec}, --encoding {std,rmana,rmana_dual,rfields,vec} encoding format to use
-r, --randomize randomize the order of symbols in mana costs
--nolinetrans don't reorder lines of card text
--nolabel don't label fields
-s, --stable don't randomize the order of the cards -s, --stable don't randomize the order of the cards
-v, --verbose verbose output -v, --verbose verbose output
``` ```
The supported encodings are: The supported encodings are:
Argument | Description Argument | Description
-----------|------------ -----------|------------
std | standard format: |name|supertypes|types|loyalty|subtypes|rarity|pt|cost|text| std | Standard format: `|type|supertype|subtype|loyalty|pt|text|cost|rarity|name|`.
rmana | randomized mana: as standard, but symbols in mana costs will be mixed: {^^UUUU} -> {UU^^UU} named | Name first: `|name|type|supertype|subtype|loyalty|pt|text|cost|rarity|`.
rmana_dual | as rmana, but with a second mana cost field after the text field noname | No name field at all: `|type|supertype|subtype|loyalty|pt|text|cost|rarity|`.
rfields | randomize the order of the fields, and use a label to distinguish which field is which rfields | Randomize the order of the fields, using only the label to distinguish which field is which.
vec | produce a content vector for each card; used with [word2vec](https://code.google.com/p/word2vec/) old | Legacy format: `|name|supertype|type|loyalty|subtype|rarity|pt|cost|text|`. No field labels.
norarity | Older legacy format: `|name|supertype|type|loyalty|subtype|pt|cost|text|`. No field labels.
vec | Produce a content vector for each card; used with [word2vec](https://code.google.com/p/word2vec/).
custom | Blank format slot, inteded to help users add their own formats to the python source.
### decode.py ### decode.py
``` ```
usage: decode.py [-h] [-g] [-f] [-c] [-d] [--norarity] [-v] [-mse] usage: decode.py [-h] [-e {std,named,noname,rfields,old,norarity,vec,custom}]
[-g] [-f] [-c] [-d] [-v] [-mse] [-html]
infile [outfile] infile [outfile]
positional arguments: positional arguments:
@ -59,21 +66,23 @@ positional arguments:
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-e {std,named,noname,rfields,old,norarity,vec,custom}, --encoding {std,named,noname,rfields,old,norarity,vec,custom}
encoding format to use
-g, --gatherer emulate Gatherer visual spoiler -g, --gatherer emulate Gatherer visual spoiler
-f, --forum use pretty mana encoding for mtgsalvation forum -f, --forum use pretty mana encoding for mtgsalvation forum
-c, --creativity use CBOW fuzzy matching to check creativity of cards -c, --creativity use CBOW fuzzy matching to check creativity of cards
-d, --dump dump out lots of information about invalid cards -d, --dump dump out lots of information about invalid cards
--norarity the card format has no rarity field; use for legacy input
-v, --verbose verbose output -v, --verbose verbose output
-mse, --mse use Magic Set Editor 2 encoding; will output as .mse-set -mse, --mse use Magic Set Editor 2 encoding; will output as .mse-
file set file
-html, --html create a .html file with pretty forum formatting
``` ```
The default output is a text spoiler which modifies the output of the neural net as little as possible while making it human readable. Specifying the -g option will produce a prettier, Gatherer-inspired text spoiler with heavier-weight transformations applied to the text, such as capitalization. The -f option encodes mana symbols in the format used by the mtgsalvation forum; this is useful if you want to cut and paste your spoiler into a post to share it. The default output is a text spoiler which modifies the output of the neural net as little as possible while making it human readable. Specifying the -g option will produce a prettier, Gatherer-inspired text spoiler with heavier-weight transformations applied to the text, such as capitalization. The -f option encodes mana symbols in the format used by the mtgsalvation forum; this is useful if you want to cut and paste your spoiler into a post to share it.
Passing the -mse option will cause decode.py to produce both the hilarious internal MSE text format as well as an actual mse set file, which is really just a renamed zip archive. The -f and -g flags will be respected in the text that is dumped to each card's notes field. Passing the -mse option will cause decode.py to produce both the hilarious internal MSE text format as well as an actual mse set file, which is really just a renamed zip archive. The -f and -g flags will be respected in the text that is dumped to each card's notes field.
Finally, the -c and -d options will print out additional data about the quality of the cards. Running with -c is extremely slow due to the massive amount of computation involved; -d is probably a good idea to use in general unless you're trying to produce pretty output to show off. Finally, the -c and -d options will print out additional data about the quality of the cards. Running with -c is extremely slow due to the massive amount of computation involved, though at least we can do it in parallel over all of your processor cores; -d is probably a good idea to use in general unless you're trying to produce pretty output to show off. Using html mode is especially useful with -c as we can link to visual spoilers from magiccards.info.
### Examples ### Examples
@ -287,6 +296,8 @@ Several ambiguous words are resolved. Most directly, the word 'counter' as in 'c
I also reformat cards that choose between multiple things by removing the choice clause itself and instead having a delimited list of options prefixed by a number. If you could choose different numbers of things (one or both, one or more - turns out the latter is valid in all existing cases) then the number is 0, otherwise it's however many things you'd get to choose. So, 'choose one -\= effect x\= effect y' (the \ is a newline) becomes [&^ = effect x = effect y]. I also reformat cards that choose between multiple things by removing the choice clause itself and instead having a delimited list of options prefixed by a number. If you could choose different numbers of things (one or both, one or more - turns out the latter is valid in all existing cases) then the number is 0, otherwise it's however many things you'd get to choose. So, 'choose one -\= effect x\= effect y' (the \ is a newline) becomes [&^ = effect x = effect y].
Finally, some postprocessing is done to put the lines of a card's ability text into a standardized, canonical form. Lines with multiple keywords are split, and then we put all of the simple keywords first, followed by things like static or activated abilities. A few things always go first (such as equip and enchant) and a few other things always go last (such as kicker and countertype). There are various reasons for doing this transformation, and some proper science could probably come up with a better specific procedure. One of the primary motivations for putting abilities onto individual lines is that it should simplify the process of adding back in reminder text. It should be noted somewhere that the definition of a simple keyword ability vs. some other line of text is that a simple keyword won't contain a period, and we can split a line with multiple of them by looking for commas and semicolons.
====== ======
Here's an attempt at a list of all the things I do: Here's an attempt at a list of all the things I do:
@ -318,3 +329,5 @@ Here's an attempt at a list of all the things I do:
* Replace acutal newline characters with \ so that we can use those to separate cards * Replace acutal newline characters with \ so that we can use those to separate cards
* Clean all the unicode junk like accents and unicode minus signs out of the text so there are fewer characters * Clean all the unicode junk like accents and unicode minus signs out of the text so there are fewer characters
* Split composite text lines (i.e. "flying, first strike" -> "flying\first strike") and put the lines into canonical order

Binary file not shown.

File diff suppressed because it is too large Load diff

1
data/mtgvocab.json Normal file
View file

@ -0,0 +1 @@
{"idx_to_token": {"1": "\n", "2": " ", "3": "\"", "4": "%", "5": "&", "6": "'", "7": "*", "8": "+", "9": ",", "10": "-", "11": ".", "12": "/", "13": "0", "14": "1", "15": "2", "16": "3", "17": "4", "18": "5", "19": "6", "20": "7", "21": "8", "22": "9", "23": ":", "24": "=", "25": "@", "26": "A", "27": "B", "28": "C", "29": "E", "30": "G", "31": "L", "32": "N", "33": "O", "34": "P", "35": "Q", "36": "R", "37": "S", "38": "T", "39": "U", "40": "W", "41": "X", "42": "Y", "43": "[", "44": "\\", "45": "]", "46": "^", "47": "a", "48": "b", "49": "c", "50": "d", "51": "e", "52": "f", "53": "g", "54": "h", "55": "i", "56": "j", "57": "k", "58": "l", "59": "m", "60": "n", "61": "o", "62": "p", "63": "q", "64": "r", "65": "s", "66": "t", "67": "u", "68": "v", "69": "w", "70": "x", "71": "y", "72": "z", "73": "{", "74": "|", "75": "}", "76": "~"}, "token_to_idx": {"\n": 1, " ": 2, "\"": 3, "%": 4, "'": 6, "&": 5, "+": 8, "*": 7, "-": 10, ",": 9, "/": 12, ".": 11, "1": 14, "0": 13, "3": 16, "2": 15, "5": 18, "4": 17, "7": 20, "6": 19, "9": 22, "8": 21, ":": 23, "=": 24, "A": 26, "@": 25, "C": 28, "B": 27, "E": 29, "G": 30, "L": 31, "O": 33, "N": 32, "Q": 35, "P": 34, "S": 37, "R": 36, "U": 39, "T": 38, "W": 40, "Y": 42, "X": 41, "[": 43, "]": 45, "\\": 44, "^": 46, "a": 47, "c": 49, "b": 48, "e": 51, "d": 50, "g": 53, "f": 52, "i": 55, "h": 54, "k": 57, "j": 56, "m": 59, "l": 58, "o": 61, "n": 60, "q": 63, "p": 62, "s": 65, "r": 64, "u": 67, "t": 66, "w": 69, "v": 68, "y": 71, "x": 70, "{": 73, "z": 72, "}": 75, "|": 74, "~": 76}}

File diff suppressed because it is too large Load diff

View file

@ -12,13 +12,16 @@ import cardlib
from cbow import CBOW from cbow import CBOW
from namediff import Namediff from namediff import Namediff
def exclude_sets(cardset):
return cardset == 'Unglued' or cardset == 'Unhinged' or cardset == 'Celebration'
def main(fname, oname = None, verbose = True, encoding = 'std', def main(fname, oname = None, verbose = True, encoding = 'std',
gatherer = False, for_forum = False, for_mse = False, gatherer = False, for_forum = False, for_mse = False,
creativity = False, vdump = False, for_html = False): creativity = False, vdump = False, for_html = False):
# there is a sane thing to do here (namely, produce both at the same time)
# but we don't support it yet.
if for_mse and for_html:
print 'ERROR - decode.py - incompatible formats "mse" and "html"'
return
fmt_ordered = cardlib.fmt_ordered_default fmt_ordered = cardlib.fmt_ordered_default
if encoding in ['std']: if encoding in ['std']:
@ -46,8 +49,36 @@ def main(fname, oname = None, verbose = True, encoding = 'std',
cards = jdecode.mtg_open_file(fname, verbose=verbose, fmt_ordered=fmt_ordered) cards = jdecode.mtg_open_file(fname, verbose=verbose, fmt_ordered=fmt_ordered)
if creativity: if creativity:
cbow = CBOW()
namediff = Namediff() namediff = Namediff()
cbow = CBOW()
if verbose:
print 'Computing nearest names...'
nearest_names = namediff.nearest_par(map(lambda c: c.name, cards), n=3)
if verbose:
print 'Computing nearest cards...'
nearest_cards = cbow.nearest_par(cards)
for i in range(0, len(cards)):
cards[i].nearest_names = nearest_names[i]
cards[i].nearest_cards = nearest_cards[i]
if verbose:
print '...Done.'
def hoverimg(cardname, dist, nd):
truename = nd.names[cardname]
code = nd.codes[cardname]
namestr = ''
if for_html:
if code:
namestr = ('<div class="hover_img"><a href="#">' + truename
+ '<span><img src="http://magiccards.info/scans/en/' + code
+ '" alt="image"/></span></a>' + ': ' + str(dist) + '</div>')
else:
namestr = '<div>' + truename + ': ' + str(dist) + '</div>'
elif for_forum:
namestr = '[card]' + truename + '[/card]' + ': ' + str(dist) + '\n'
else:
namestr = truename + ': ' + str(dist) + '\n'
return namestr
def writecards(writer): def writecards(writer):
if for_mse: if for_mse:
@ -68,31 +99,30 @@ def main(fname, oname = None, verbose = True, encoding = 'std',
fstring += 'raw:\n' + card.raw + '\n' fstring += 'raw:\n' + card.raw + '\n'
fstring += '\n' fstring += '\n'
fstring += card.format(gatherer = gatherer, for_forum = for_forum, fstring += card.format(gatherer = gatherer, for_forum = for_forum,
vdump = vdump) vdump = vdump) + '\n'
fstring = fstring.replace('<', '(').replace('>', ')') fstring = fstring.replace('<', '(').replace('>', ')')
writer.write(('\n' + fstring[:-1]).replace('\n', '\n\t\t')) writer.write(('\n' + fstring[:-1]).replace('\n', '\n\t\t'))
else: else:
writer.write(card.format(gatherer = gatherer, for_forum = for_forum, fstring = card.format(gatherer = gatherer, for_forum = for_forum,
vdump = vdump, for_html = for_html).encode('utf-8')) vdump = vdump, for_html = for_html)
if creativity and for_html:
fstring = fstring[:-6] # chop off the closing </div> to stick stuff in
writer.write((fstring + '\n').encode('utf-8'))
if creativity: if creativity:
cstring = '~~ closest cards ~~\n' cstring = '~~ closest cards ~~\n'
nearest = cbow.nearest(card) nearest = card.nearest_cards
for dist, cardname in nearest: for dist, cardname in nearest:
cardname = namediff.names[cardname] cstring += hoverimg(cardname, dist, namediff)
if for_forum:
cardname = '[card]' + cardname + '[/card]'
cstring += cardname + ': ' + str(dist) + '\n'
cstring += '~~ closest names ~~\n' cstring += '~~ closest names ~~\n'
nearest = namediff.nearest(card.name) nearest = card.nearest_names
for dist, cardname in nearest: for dist, cardname in nearest:
cardname = namediff.names[cardname] cstring += hoverimg(cardname, dist, namediff)
if for_forum: if for_html:
cardname = '[card]' + cardname + '[/card]' cstring = '<hr><div>' + cstring.replace('\n', '<br>\n') + '</div>\n</div>'
cstring += cardname + ': ' + str(dist) + '\n' elif for_mse:
if for_mse:
cstring = cstring.replace('<', '(').replace('>', ')')
cstring = ('\n\n' + cstring[:-1]).replace('\n', '\n\t\t') cstring = ('\n\n' + cstring[:-1]).replace('\n', '\n\t\t')
writer.write(cstring.encode('utf-8')) writer.write(cstring.encode('utf-8'))
writer.write('\n'.encode('utf-8')) writer.write('\n'.encode('utf-8'))
@ -159,8 +189,11 @@ if __name__ == '__main__':
parser.add_argument('-mse', '--mse', action='store_true', parser.add_argument('-mse', '--mse', action='store_true',
help='use Magic Set Editor 2 encoding; will output as .mse-set file') help='use Magic Set Editor 2 encoding; will output as .mse-set file')
parser.add_argument('-html', '--html', action='store_true', help='create a .html file with pretty forum formatting') parser.add_argument('-html', '--html', action='store_true', help='create a .html file with pretty forum formatting')
args = parser.parse_args() args = parser.parse_args()
main(args.infile, args.outfile, verbose = args.verbose, encoding = args.encoding, main(args.infile, args.outfile, verbose = args.verbose, encoding = args.encoding,
gatherer = args.gatherer, for_forum = args.forum, for_mse = args.mse, gatherer = args.gatherer, for_forum = args.forum, for_mse = args.mse,
creativity = args.creativity, vdump = args.dump, for_html = args.html) creativity = args.creativity, vdump = args.dump, for_html = args.html)
exit(0) exit(0)

View file

@ -257,9 +257,12 @@ def fields_from_json(src_json, linetrans = True):
parsed = False parsed = False
if 'subtypes' in src_json: if 'subtypes' in src_json:
fields[field_subtypes] = [(-1, map(lambda s: utils.to_ascii(s.lower()), fields[field_subtypes] = [(-1, map(lambda s: utils.to_ascii(s.lower())
# urza's lands...
.replace('"', "'").replace('-', utils.dash_marker),
src_json['subtypes']))] src_json['subtypes']))]
if 'rarity' in src_json: if 'rarity' in src_json:
if src_json['rarity'] in utils.json_rarity_map: if src_json['rarity'] in utils.json_rarity_map:
fields[field_rarity] = [(-1, utils.json_rarity_map[src_json['rarity']])] fields[field_rarity] = [(-1, utils.json_rarity_map[src_json['rarity']])]
@ -389,13 +392,19 @@ def fields_from_format(src_text, fmt_ordered, fmt_labeled, fieldsep):
class Card: class Card:
'''card representation with data''' '''card representation with data'''
def __init__(self, src, fmt_ordered = fmt_ordered_default, fmt_labeled = fmt_labeled_default, fieldsep = utils.fieldsep, linetrans = True): def __init__(self, src, fmt_ordered = fmt_ordered_default,
fmt_labeled = fmt_labeled_default,
fieldsep = utils.fieldsep, linetrans = True):
# source fields, exactly one will be set # source fields, exactly one will be set
self.json = None self.json = None
self.raw = None self.raw = None
# flags # flags
self.parsed = True self.parsed = True
self.valid = True # doesn't record that much self.valid = True # doesn't record that much
# placeholders to fill in with expensive distance metrics
self.nearest_names = []
self.nearest_cards = []
# default values for all fields # default values for all fields
self.__dict__[field_name] = '' self.__dict__[field_name] = ''
self.__dict__[field_rarity] = '' self.__dict__[field_rarity] = ''
@ -413,6 +422,7 @@ class Card:
self.__dict__[field_text] = Manatext('') self.__dict__[field_text] = Manatext('')
self.__dict__[field_text + '_lines'] = [] self.__dict__[field_text + '_lines'] = []
self.__dict__[field_text + '_words'] = [] self.__dict__[field_text + '_words'] = []
self.__dict__[field_text + '_lines_words'] = []
self.__dict__[field_other] = [] self.__dict__[field_other] = []
self.bside = None self.bside = None
# format-independent view of processed input # format-independent view of processed input
@ -539,6 +549,9 @@ class Card:
self.__dict__[field_text + '_words'] = re.sub(utils.unletters_regex, self.__dict__[field_text + '_words'] = re.sub(utils.unletters_regex,
' ', ' ',
fulltext).split() fulltext).split()
self.__dict__[field_text + '_lines_words'] = map(
lambda line: re.sub(utils.unletters_regex, ' ', line).split(),
fulltext.split(utils.newline))
else: else:
self.valid = False self.valid = False
self.__dict__[field_other] += [(idx, '<text> ' + str(value))] self.__dict__[field_other] += [(idx, '<text> ' + str(value))]
@ -552,7 +565,9 @@ class Card:
# the NN representation, use str() or format() for output intended for human # the NN representation, use str() or format() for output intended for human
# readers. # readers.
def encode(self, fmt_ordered = fmt_ordered_default, fmt_labeled = None, fieldsep = utils.fieldsep, randomize_fields = False, randomize_mana = False, initial_sep = True, final_sep = True): def encode(self, fmt_ordered = fmt_ordered_default, fmt_labeled = fmt_labeled_default,
fieldsep = utils.fieldsep, initial_sep = True, final_sep = True,
randomize_fields = False, randomize_mana = False, randomize_lines = False):
outfields = [] outfields = []
for field in fmt_ordered: for field in fmt_ordered:
@ -566,6 +581,8 @@ class Card:
outfield_str = outfield.encode(randomize = randomize_mana) outfield_str = outfield.encode(randomize = randomize_mana)
elif isinstance(outfield, Manatext): elif isinstance(outfield, Manatext):
outfield_str = outfield.encode(randomize = randomize_mana) outfield_str = outfield.encode(randomize = randomize_mana)
if randomize_lines:
outfield_str = transforms.randomize_lines(outfield_str)
else: else:
outfield_str = outfield outfield_str = outfield
else: else:
@ -599,19 +616,31 @@ class Card:
return outstr return outstr
def format(self, gatherer = False, for_forum = False, for_mse = False, vdump = False, for_html = False): def format(self, gatherer = False, for_forum = False, vdump = False, for_html = False):
linebreak = '\n'
if for_html:
linebreak = '<hr>' + linebreak
outstr = '' outstr = ''
if for_html:
outstr += '<div class="card-text">\n'
if gatherer: if gatherer:
cardname = titlecase(transforms.name_unpass_1_dashes(self.__dict__[field_name])) cardname = titlecase(transforms.name_unpass_1_dashes(self.__dict__[field_name]))
if vdump and not cardname: if vdump and not cardname:
cardname = '_NONAME_' cardname = '_NONAME_'
if for_forum: # in general, for_html overrides for_forum
if for_html:
outstr += '<b>'
elif for_forum:
outstr += '[b]' outstr += '[b]'
outstr += cardname outstr += cardname
if for_forum: if for_html:
outstr += '</b>'
elif for_forum:
outstr += '[/b]' outstr += '[/b]'
coststr = self.__dict__[field_cost].format(for_forum = for_forum) coststr = self.__dict__[field_cost].format(for_forum=for_forum, for_html=for_html)
if vdump or not coststr == '_NOCOST_': if vdump or not coststr == '_NOCOST_':
outstr += ' ' + coststr outstr += ' ' + coststr
@ -628,7 +657,7 @@ class Card:
if not self.valid: if not self.valid:
outstr += ' _INVALID_' outstr += ' _INVALID_'
outstr += '\n' outstr += linebreak
basetypes = map(str.capitalize, self.__dict__[field_types]) basetypes = map(str.capitalize, self.__dict__[field_types])
if vdump and len(basetypes) < 1: if vdump and len(basetypes) < 1:
@ -646,9 +675,9 @@ class Card:
if self.__dict__[field_loyalty]: if self.__dict__[field_loyalty]:
outstr += ' ((' + utils.from_unary(self.__dict__[field_loyalty]) + '))' outstr += ' ((' + utils.from_unary(self.__dict__[field_loyalty]) + '))'
outstr += '\n'
if self.__dict__[field_text].text: if self.__dict__[field_text].text:
outstr += linebreak
mtext = self.__dict__[field_text].text mtext = self.__dict__[field_text].text
mtext = transforms.text_unpass_1_choice(mtext, delimit = False) mtext = transforms.text_unpass_1_choice(mtext, delimit = False)
mtext = transforms.text_unpass_2_counters(mtext) mtext = transforms.text_unpass_2_counters(mtext)
@ -662,109 +691,68 @@ class Card:
newtext = Manatext('') newtext = Manatext('')
newtext.text = mtext newtext.text = mtext
newtext.costs = self.__dict__[field_text].costs newtext.costs = self.__dict__[field_text].costs
outstr += newtext.format(for_forum = for_forum)
outstr += '\n' outstr += newtext.format(for_forum = for_forum, for_html = for_html)
if vdump and self.__dict__[field_other]: if vdump and self.__dict__[field_other]:
if for_forum: outstr += linebreak
if for_html:
outstr += '<i>'
elif for_forum:
outstr += '[i]' outstr += '[i]'
else: else:
outstr += utils.dash_marker * 2 outstr += utils.dash_marker * 2
outstr += '\n'
first = True
for idx, value in self.__dict__[field_other]: for idx, value in self.__dict__[field_other]:
outstr += '<' + str(idx) + '> ' + str(value) if for_html:
outstr += '\n' if not first:
if for_forum: outstr += '<br>\n'
outstr = outstr[:-1] # hack off the last newline
outstr += '[/i]'
outstr += '\n'
elif for_html:
outstr += '<div class="card-text">'
cardname = self.__dict__[field_name]
#cardname = transforms.name_unpass_1_dashes(self.__dict__[field_name])
if vdump and not cardname:
cardname = '_NONAME_'
outstr += cardname + ' '
coststr = self.__dict__[field_cost].format(for_html = for_html)
if vdump or not coststr == '_NOCOST_':
outstr += coststr
outstr += '<br>'
if self.__dict__[field_rarity]:
if self.__dict__[field_rarity] in utils.json_rarity_unmap:
rarity = utils.json_rarity_unmap[self.__dict__[field_rarity]]
else: else:
rarity = self.__dict__[field_rarity] first = False
outstr += ' (' + rarity.lower() + ') ' else:
outstr += '\n<hr><b>' outstr += linebreak
outstr += '(' + str(idx) + ') ' + str(value)
outstr += ' '.join(self.__dict__[field_supertypes] + self.__dict__[field_types]) if for_html:
if self.__dict__[field_subtypes]: outstr += '</i>'
outstr += ' ' + utils.dash_marker + ' ' + ' '.join(self.__dict__[field_subtypes]) if for_forum:
outstr += '</b><hr>\n' outstr += '[/i]'
if self.__dict__[field_text].text:
mtext = self.__dict__[field_text].text
mtext = transforms.text_unpass_1_choice(mtext, delimit = True)
#mtext = transforms.text_unpass_2_counters(mtext)
#mtext = transforms.text_unpass_3_uncast(mtext)
mtext = transforms.text_unpass_4_unary(mtext)
mtext = transforms.text_unpass_5_symbols(mtext,for_forum, for_html)
#mtext = transforms.text_unpass_6_cardname(mtext, cardname)
mtext = transforms.text_unpass_7_newlines(mtext).replace("\n", "<br>")
#mtext = transforms.text_unpass_8_unicode(mtext)
newtext = Manatext('')
newtext.text = mtext
newtext.costs = self.__dict__[field_text].costs
outstr += newtext.format(for_html = for_html) + '\n'
if self.__dict__[field_pt]:
outstr += '<br>(' + utils.from_unary(self.__dict__[field_pt]) + ')<br>'
outstr += '\n'
if self.__dict__[field_loyalty]:
outstr += '((' + utils.from_unary(self.__dict__[field_loyalty]) + '))'
outstr += '\n'
if vdump and self.__dict__[field_other]:
outstr += utils.dash_marker * 2
outstr += '\n'
for idx, value in self.__dict__[field_other]:
outstr += '<' + str(idx) + '> ' + str(value)
outstr += '\n'
else: else:
cardname = self.__dict__[field_name] cardname = self.__dict__[field_name]
#cardname = transforms.name_unpass_1_dashes(self.__dict__[field_name]) #cardname = transforms.name_unpass_1_dashes(self.__dict__[field_name])
if vdump and not cardname: if vdump and not cardname:
cardname = '_NONAME_' cardname = '_NONAME_'
outstr += cardname outstr += cardname
coststr = self.__dict__[field_cost].format(for_forum=for_forum, for_html=for_html)
if vdump or not coststr == '_NOCOST_':
outstr += ' ' + coststr
if vdump:
if not self.parsed:
outstr += ' _UNPARSED_'
if not self.valid:
outstr += ' _INVALID_'
outstr += linebreak
outstr += ' '.join(self.__dict__[field_supertypes] + self.__dict__[field_types])
if self.__dict__[field_subtypes]:
outstr += ' ' + utils.dash_marker + ' ' + ' '.join(self.__dict__[field_subtypes])
if self.__dict__[field_rarity]: if self.__dict__[field_rarity]:
if self.__dict__[field_rarity] in utils.json_rarity_unmap: if self.__dict__[field_rarity] in utils.json_rarity_unmap:
rarity = utils.json_rarity_unmap[self.__dict__[field_rarity]] rarity = utils.json_rarity_unmap[self.__dict__[field_rarity]]
else: else:
rarity = self.__dict__[field_rarity] rarity = self.__dict__[field_rarity]
outstr += ' (' + rarity.lower() + ')' outstr += ' (' + rarity.lower() + ')'
if vdump:
if not self.parsed:
outstr += ' _UNPARSED_'
if not self.valid:
outstr += ' _INVALID_'
outstr += '\n'
coststr = self.__dict__[field_cost].format(for_forum = for_forum)
if vdump or not coststr == '_NOCOST_':
outstr += coststr
outstr += '\n'
outstr += ' '.join(self.__dict__[field_supertypes] + self.__dict__[field_types])
if self.__dict__[field_subtypes]:
outstr += ' ' + utils.dash_marker + ' ' + ' '.join(self.__dict__[field_subtypes])
outstr += '\n'
if self.__dict__[field_text].text: if self.__dict__[field_text].text:
outstr += linebreak
mtext = self.__dict__[field_text].text mtext = self.__dict__[field_text].text
mtext = transforms.text_unpass_1_choice(mtext, delimit = True) mtext = transforms.text_unpass_1_choice(mtext, delimit = True)
#mtext = transforms.text_unpass_2_counters(mtext) #mtext = transforms.text_unpass_2_counters(mtext)
@ -777,31 +765,60 @@ class Card:
newtext = Manatext('') newtext = Manatext('')
newtext.text = mtext newtext.text = mtext
newtext.costs = self.__dict__[field_text].costs newtext.costs = self.__dict__[field_text].costs
outstr += newtext.format(for_forum = for_forum) + '\n'
outstr += newtext.format(for_forum=for_forum, for_html=for_html)
if self.__dict__[field_pt]: if self.__dict__[field_pt]:
outstr += linebreak
outstr += '(' + utils.from_unary(self.__dict__[field_pt]) + ')' outstr += '(' + utils.from_unary(self.__dict__[field_pt]) + ')'
outstr += '\n'
if self.__dict__[field_loyalty]: if self.__dict__[field_loyalty]:
outstr += linebreak
outstr += '((' + utils.from_unary(self.__dict__[field_loyalty]) + '))' outstr += '((' + utils.from_unary(self.__dict__[field_loyalty]) + '))'
outstr += '\n'
if vdump and self.__dict__[field_other]: if vdump and self.__dict__[field_other]:
outstr += linebreak
if for_html:
outstr += '<i>'
else:
outstr += utils.dash_marker * 2 outstr += utils.dash_marker * 2
outstr += '\n'
first = True
for idx, value in self.__dict__[field_other]: for idx, value in self.__dict__[field_other]:
outstr += '<' + str(idx) + '> ' + str(value) if for_html:
outstr += '\n' if not first:
outstr += '<br>\n'
else:
first = False
else:
outstr += linebreak
outstr += '(' + str(idx) + ') ' + str(value)
if for_html:
outstr += '</i>'
if self.bside: if self.bside:
if for_html: if for_html:
outstr += "<hr><hr>\n" outstr += '\n'
# force for_forum to false so that the inner div doesn't duplicate the forum
# spoiler of the bside
outstr += self.bside.format(gatherer=gatherer, for_forum=False, for_html=for_html, vdump=vdump)
else: else:
outstr += utils.dash_marker * 8 + '\n' outstr += linebreak
outstr += self.bside.format(gatherer = gatherer, for_forum = for_forum, for_html = for_html) outstr += utils.dash_marker * 8
outstr += linebreak
outstr += self.bside.format(gatherer=gatherer, for_forum=for_forum, for_html=for_html, vdump=vdump)
if for_html: if for_html:
if for_forum:
outstr += linebreak
# force for_html to false to create a copyable forum spoiler div
outstr += ('<div>'
+ self.format(gatherer=gatherer, for_forum=for_forum, for_html=False, vdump=vdump).replace('\n', '<br>')
+ '</div>')
outstr += "</div>" outstr += "</div>"
return outstr return outstr
def to_mse(self, print_raw = False, vdump = False): def to_mse(self, print_raw = False, vdump = False):

View file

@ -8,20 +8,18 @@ import subprocess
import os import os
import struct import struct
import math import math
import multiprocessing
import utils import utils
import cardlib import cardlib
import transforms import transforms
import namediff
# # this would be nice, but doing it naively makes things worse
# from joblib import Parallel, delayed
# import multiprocessing
libdir = os.path.dirname(os.path.realpath(__file__)) libdir = os.path.dirname(os.path.realpath(__file__))
datadir = os.path.realpath(os.path.join(libdir, '../data')) datadir = os.path.realpath(os.path.join(libdir, '../data'))
# # multithreading control parameters # multithreading control parameters
# cores = multiprocessing.cpu_count() cores = multiprocessing.cpu_count()
# segments = cores / 2 if cores / 2 > 0 else 1
# max length of vocabulary entries # max length of vocabulary entries
max_w = 50 max_w = 50
@ -67,6 +65,11 @@ def makevector(vocabulary,vecs,sequence):
res = v res = v
else: else:
res = [x + y for x, y in zip(res,v)] res = [x + y for x, y in zip(res,v)]
# bad things happen if we have a vector of only unknown words
if res is None:
return [0.0]*len(vecs[0])
length = math.sqrt(sum([res[i] * res[i] for i in range(0,len(res))])) length = math.sqrt(sum([res[i] * res[i] for i in range(0,len(res))]))
for i in range(0,len(res)): for i in range(0,len(res)):
res[i] /= length res[i] /= length
@ -118,6 +121,33 @@ except ImportError:
def cosine_similarity_name(cardvec, v, name): def cosine_similarity_name(cardvec, v, name):
return (cosine_similarity(cardvec, v), name) return (cosine_similarity(cardvec, v), name)
# we need to put the logic in a regular function (as opposed to a method of an object)
# so that we can pass the function to multiprocessing
def f_nearest(card, vocab, vecs, cardvecs, n):
if isinstance(card, cardlib.Card):
words = card.vectorize().split('\n\n')[0]
else:
# assume it's a string (that's already a vector)
words = card
if not words:
return []
cardvec = makevector(vocab, vecs, words)
comparisons = [cosine_similarity_name(cardvec, v, name) for (name, v) in cardvecs]
comparisons.sort(reverse = True)
comp_n = comparisons[:n]
if isinstance(card, cardlib.Card) and card.bside:
comp_n += f_nearest(card.bside, vocab, vecs, cardvecs, n=n)
return comp_n
def f_nearest_per_thread(workitem):
(workcards, vocab, vecs, cardvecs, n) = workitem
return map(lambda card: f_nearest(card, vocab, vecs, cardvecs, n), workcards)
class CBOW: class CBOW:
def __init__(self, verbose = True, def __init__(self, verbose = True,
@ -148,8 +178,6 @@ class CBOW:
self.vecs, self.vecs,
card.vectorize()))] card.vectorize()))]
# self.par = Parallel(n_jobs=segments)
if self.verbose: if self.verbose:
print '... Done.' print '... Done.'
print ' vocab size: ' + str(len(self.vocab)) print ' vocab size: ' + str(len(self.vocab))
@ -157,25 +185,11 @@ class CBOW:
print ' card vecs: ' + str(len(self.cardvecs)) print ' card vecs: ' + str(len(self.cardvecs))
def nearest(self, card, n=5): def nearest(self, card, n=5):
if isinstance(card, cardlib.Card): return f_nearest(card, self.vocab, self.vecs, self.cardvecs, n)
words = card.vectorize().split('\n\n')[0]
else:
# assume it's a string (that's already a vector)
words = card
if not words: def nearest_par(self, cards, n=5, threads=cores):
return [] workpool = multiprocessing.Pool(threads)
proto_worklist = namediff.list_split(cards, threads)
cardvec = makevector(self.vocab, self.vecs, words) worklist = map(lambda x: (x, self.vocab, self.vecs, self.cardvecs, n), proto_worklist)
donelist = workpool.map(f_nearest_per_thread, worklist)
comparisons = [cosine_similarity_name(cardvec, v, name) for (name, v) in self.cardvecs] return namediff.list_flatten(donelist)
# comparisons = self.par(delayed(cosine_similarity_name)(cardvec, v, name)
# for (name, v) in self.cardvecs)
comparisons.sort(reverse = True)
comp_n = comparisons[:n]
if isinstance(card, cardlib.Card) and card.bside:
comp_n += self.nearest(card.bside)
return comp_n

View file

@ -61,3 +61,4 @@ field_label_text = '9'
# additional fields we add to the json cards # additional fields we add to the json cards
json_field_bside = 'bside' json_field_bside = 'bside'
json_field_set_name = 'setName' json_field_set_name = 'setName'
json_field_info_code = 'magicCardsInfoCode'

371
lib/html_extra_data.py Normal file
View file

@ -0,0 +1,371 @@
box_width = 350
html_prepend = """<!DOCTYPE html>
<html>
<head>
<style>
.card-text{ display:inline-block;
max-width:350px;
margin: 2px;
padding:3px;
border: 3px solid #000000;}
.hover_img a { position:relative; }
.hover_img a span { position:absolute; display:none; z-index:99; }
.hover_img a:hover span { display:block; }
.mana-0 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px; ;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEvSURBVChTY/gPBn///nn57PGNy2cvnj4CREAGkAsUhMiCFP348f36pTMXTh2eOXWin6+Pk6NDZ2sjkAsUBEqBFAGVQ1QsXTDbxsb69LH9u7dtkJGV625vhqgDKmAAGgvkAJG9vV1bcz2EnZGWoqqqdv7kISAbqIABaD2QdWTfDj4+gWUL50AUTZnQw8PLv2ntciAbqIAB6Ewga/H8WXBRIFq1bAGQO3/2NCAbqACqCMgHiq5ethCiaO3KxUDu4nkzoYog1m1etwIoOn1yP0QR0BO8fAIH92wDskHWwR2uqamVnZkOYQN9YGNtDWGDHA4Pggk9HfIKikAfnDtx0N7ODmIXNAiQA7OrvdnXxzsiLHTapD6ICmhgAjEQAJUDjQVaD3QmEAEZQC5QECT3/z8A05MwgYZeWs0AAAAASUVORK5CYII=') no-repeat;
}
.mana-1 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADjSURBVChTjZGxDgFBFEXHE7MrthFClBrNRvyTSET4CBIaCo3GT4jCT5AoNRoJdtcK0bFjY9dlZnXErd579+TNzJ1YGIaMsSB4nFzncj4K74ZW05PpTC6bLxDF0b4gIbzNeuXdrs7BnS+W9sFt1qsSLZZMTdMJO0Bgge/7k+lsOBrvLRsEhCEsAIRT5BGc81ajVimbkpCCBYBwDzV4yzBSqooEgOSaHwJAqvwpwhNU+UUACHmoLpJM7iMAhMQ+y2BbtrPdWUHEwXpHSvFXYnpSiHunN0DPeaLd7aPFEBaAP76FsSe9yn/QI2WYAgAAAABJRU5ErkJggg==') no-repeat;
}
.mana-2{
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFPSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpTed+Dwuk1bf/z4aWttERsZysnFraimzc7OwQQ0A6Ji+669y1at8/FwMzLQmz1v8ez5i4GCQCmgAhagLUDOr1+/tu3YM6W/g4OdHWj+3Xv39+w7lJ4cD5QCKmACugMo+uPnz9ysFIgKIBAVEeHl4YGwgQqYgGqBLD5eXjUVZYjo589fDh89HhbsD+ECFTBBWHDw+8+f+pbOAD9vDzcnqBADAxPQt1AmKCD+dfRMcnN2TE2MgQqBg4MJGB4QDjAspsyY6+3hAjHj3IVLDx89ATKAChj//v1z6+qFH9+/9U6cBnQ+0P9AiffvP6zftG3JvGn8AoJq2gYswDAFhtiGNSuvXLsBlL5z9z6QBAIzE0OgCqAUUAER0cLAAAAj1rLKRkR9sgAAAABJRU5ErkJggg==') no-repeat;
}
.mana-3{
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFUSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpQ+efrc2g1b3r57Z6ink5oUx8fPr6imzc7OwQQ0A6Jiw+btG7dsj48OC/L3WbFmw5btu4CCQCmgAiagLRAreLi5WhuqtLU0PN2cODk5RISFgIJAKaACxhuXz/74/g3Ih4Dfv39Pmjabj48vNTEGIsLBycUCMQYC7t5/0Nk7+dWr11+/fRMU4A8J9AUKAhUwXjx9BOJBIHj1+g0vDw8HB3v3hKnbd+zZvHYpDw83IyMjE9C3EBVAICYqAnQNUNTN2eH3nz9v3r4FCgIVMAHDA6ICGdy//1BQQEBGWgrIBipgAoYYUC3QxucvXkJUnDh1Zt6i5dXlBSwsLEApoAJoYF69eGb6jDmfv3z58vUbHy9vZFignKwMUAUkMImIFgYGALORsD4EQl2jAAAAAElFTkSuQmCC') no-repeat;
}
.mana-4{
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAE3SURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpEGcpvbe9nY2CpK8oBKFdW02dk5mIBmwFUAwfpNW48cP/nk2TMgGygIlAIqYALaAlfx5Omzu/ceqqkqQ7hAAJQCKmACugPC//fv38w5CzNTEyBcOAAqYIIbs3TFWk93Fx4ebggXDoAKmCCsO3fvP3/50srCFMIFgrPnL756/QbCZrxx+eyP79/aeyZ+/vyFm4sLKHTy9Nl/////+/tv2YIZAgL8HJxcjC+fPX7+5MHHj5++fYfaW9/SBSQba8okJcSBDEkZBSZgiAHDg5+fDygERBLiYj9+/Pj06TOQAVQBlAIHKRMzKMQ4OEGGMDC0dPYDhTg42IFBCglMoAIiooWBAQBwdqfO6tnExwAAAABJRU5ErkJggg==') no-repeat;
}
.mana-5{
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEpSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpRu6ez7+vUbUBQIfv36NWVCt6KaNjs7BxPQDIiKV6/fvHz5WkJcDIIszEyAgkApoALGl88eP3/yAKh1z/5D/Hy8psaGEJPgQFJGgQXoDgjn0uVrFy5dfvT4qZCgQHCAT1R4MCMjI1AcqIAF4lIIiIsOFxURPnDo6PTZC/7++xcXFQYUBCpgvHj6CMSDyKCyruXajVsbVy0CsoHmMQF9C5FABloa6h8/foKwgQqYgOEB4SCDazduaqipQNhABUzAEEM27PefPwuXrjx3/lJeViqQC5QCKkAEZntX/7fv396//yArIx0eEiApIQ5UAQlMIqKFgQEAnxOfpbljyQoAAAAASUVORK5CYII=') no-repeat;
}
.mana-6{
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFgSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpF+9frN2vWb33/4GBcdrqyspKimzc7Owfj3759bVy9AVBw5dnLuwqWlhTlaGmpALhAAjVTTNmB8+ezx8ycPgPx79x9m5pXOnzVJSlICogICJGUUGG9cPvvj+zcgp7KuhYWZWV9P58q1GypKCuGhgawsLEBxDk4uJohFv//8OXHyzI1bd2SkJYP8vVeu2dg3cTrIHAYGoAImCOvdu/dAdSkJ0RZmJno6WoH+Xtt27vnz5w9ElgnoNCDFxQkiGRgZQSQDg4Kc7N+/f9+9/wBkAxUwAcMDyOLl5VFRVrxx8zZYDcOXr9+4ubiEhQSBbKACJmCIQQzLSInfs+/Qm7fv/v37t2vv/qjwIGZmZqAUUAFKYB47cWrL9t2MjIzamuqRYUFAf0EDk3C0MDAAACK6pimZEg74AAAAAElFTkSuQmCC') no-repeat;
}
.mana-7 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEVSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+62tM/+fGTZ8zMTEBRCCgpyLF2cGFn52ACmgFUATTgwKGjHBzsEuJiEHT7zj0ODjagFFABC9AWoIr3Hz64OjtkpMRDzDh4+Ji4mCg3FxdQCqiA8cblsz++f4PIQcCfP39SsgqnTeji4uIEcjk4uZggLkUGew8cVlNRhqgAAqAChDPhYNnKtc6OdlAOGDABfQtlgsGlK9cePXqir6cN5YODgwkYHlAeGOzcvV9bS4ODnR3KZ2AAKmAChhiyYWfPX1RVUYJywMaAg5SJWVFNG66Oh4dbSVEewgYKAqWACoiIFgYGAIXReK4bpsD6AAAAAElFTkSuQmCC') no-repeat;
}
.mana-8 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFrSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6+v3b1+Wr1t24defnz5/aWhqRoYG8fPyKatrs7ByMf//+uXX1AtCAvkkz/vz9U1KQ/e/v396J03/9/l1bUQQ0Uk3bgAloC8SKo8dPamuoMzEysrCwuDrbX712HSgIlAIqYAK6A8gBAg111RlzFu7Zf+jv37+79x709nCDiAMVMF48fQTi9i9fv7Z09B05dpKTk8PJwbayJB+iiJGRkQnCAoKHDx///v27oaZMR0tz6/bd8xYtg0oA1d24fPbH929//vwJDE/obKnV0lQHigK9OXPuoh2bVnIA/cbJxQQMD6Dop0+f33/4ICYmCtbJEB4aCPTjt6/fgGygAiZgiAEVCwkJ6mprrt+0DaLoxMnTBvq6QEGgFFABIjDfvXu7aOmqN2/f8XBzcXBwxEWHCQkJQwOTcLQwMAAAqCC9xuBqHPkAAAAASUVORK5CYII=') no-repeat;
}
.mana-9 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFeSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6+uP7t+Wr1l24dIWZmdnfx8PCzASoVFFNm52dg+Hv3z/XL525cOpwUUGulaXl2eMHdmxeKyUlM2vaJKAgUAqogAloC9CKf//+AY1xc3EEGiMhLubqbD9jzgKgRUApoAImoDuAnDdv333+/IWPlwfIBgItDbVbt+9+/fYNyAYqYIK4VEhQAGjGw8dPwGqATmUEEl+/ghQBFTCBxRhYWFi83F02bN5+/ebt02fPz5m/BCgiwM8HkWUuyMv+8+c3kGVuZvz71++Dh4+xsrL+//dPTFTU090ZKM7BycX48tnj508egDVAwes3b+NSsid2t6qpKgO5kjIKTMAQA4YHRBoIbt25W1nXUlaUC1EBlAIqQATm3bv3Fi9fxcfLG+TvLSkhDlEBCUwiooWBAQCs87JoDmJq4gAAAABJRU5ErkJggg==') no-repeat;
}
.mana-10 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAE9SURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpFGBkClimra7OwcLEAzgCqOHT+x/+ARH083DXXV6zdv79i19/PnL24uDkClatoGLEBbgGZ8//5j6449zo52P378mDhl5tQJnUCR6KRMFWUlIRFxJqA7gMptrMylJSWAjMNHT8jJyjAzM/PwcGuoqezasx+ogAnuFBZWFiB56849bm4uiIikhPiTp8+ACpggfDj49u37v3//IGw2NjZ2dnYggwnoBYgQBCjIyz599gLC/vLli6qKElABEzA8IEKfPn3+8uWrq5P9rTt3P33+/O///3v3H9rbWgEVMP79++fW1QsTJk07eeacmKhIamLsx0+fdu87KMDHB/SNiYkxMAiICkwiooWBAQCBlamF9Pj2KwAAAABJRU5ErkJggg==') no-repeat;
}
.mana-11 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAD0SURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpFGBkClimra7OwcTEAzgCoePXq0ccv2tq4JQLmnz56vXLNh+qz5QG1AKaACJqAtQA4jI+OVqzdu3bkLVMTExHT2/KVHT54C2UApoAImoDuAHDFRETsbSyADCCQlxE2M9CFsIAAqYMJ0ChoAKmCCMvECJqAXoEwcAKiACRgeEM73HyAAYf/+/fvr128QNlABc2dX98f3b69du7F2w+afv359+fL19+8/23bu/fL1Kwszi66ujqyiKlGBSUS0MDAAAJsNl49choAcAAAAAElFTkSuQmCC') no-repeat;
}
.mana-12 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEySURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpFGBkClimra7OwcjH///rl19cLly1cOHz1hamKop6P16PGTNeu3MDMzhYcEyMvLq2kbMAFtAZrx+s3bFWvWf/z46c3bd9NnL7C3tfz582decRVQCqiABegOoMnWlmbSUpJAxsnT58qLcgUE+BUV5Ddt3fnp82eOt6+Y0Jzi7eECVAFk3Lh5S1dHi4+XF6iACSKHBp48fb5y7cbGmjIIlwnoBQgLDl69frNm/ab2xhphIUGgK4EKmIDhAZH79u3716/fPnz42DtxmpWF2c3bd6bNmn/vwUOgAmgQzFu45MrV61xcXCzMTE+fv4BoY2VhndTXoaVvQlxgEo4WBgYAuEeubJyWa+YAAAAASUVORK5CYII=') no-repeat;
}
.mana-13 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAE1SURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpFGBkClimra7OwcLEAzgCrevH594dKVr1+/ebg5ff/+Y+2GzU+ePnd2tAUqVdM2YALaAjTjxctXC5euvHn7zq9fv3omTNXUUDM21KtpaAdKARUwAd0BVK6qomRlbgpksLKyFudnGRvqc3FxOdrbAEWACpjQnMLIyMjBybF42eqOnkl///4FWg1UwASVRAJMjIye7s7rVi54+PjJlu27QCJAL0DkkIGIsBArC4ucrDTQ70AFTMDwgEh8+/bt7bv3v3//BloB5J4+e/7uvQde7i5ABYx///65dfXCgYOHt+7YDXSEpbnp5y9fPn36zMfHG+DrKSQkDAwCogKTiGhhYAAAExGx/k7dMTQAAAAASUVORK5CYII=') no-repeat;
}
.mana-14 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEuSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpFGBkClimra7OwcTEAzgCoePHiwet2ms+cvQuUZGLbt3LNl63agFFABE9AWoBlPnj5buHTly1evISru3L2/eevOazduAaWACpiA7gCKmhobammoQVT8/v17zfrNYcH+EC5QARPcKczMIDcCweJlq4EqWFhYIFygAiYICw6uXrvx9+9fMVGR7z9+/PnzB+gyoCAL0As/vn+DqACCd+8/vHr9ZuLUWddv3AJyjQz1gQpYgOHx/MkDIP/Dx08fP37ycnextbYAcls7+5mYmRztrIEKmIAhBlS7YfN2AX6+W3fu3b0P0nD56nWgCiDj5u17QAVEBSYR0cLAAAAOyLDdh7oV9AAAAABJRU5ErkJggg==') no-repeat;
}
.mana-15 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEySURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpFGBkClimra7OwcjH///rl28cyuPfv27DuYnhynIC83efqcZ89fABV9/PhpYm+Hkbk1C9CW379+srOxHTl2Miku6tevX3/+/AkL9gcqYmJiYmVhAipgAroDyLGxMufm4gJKXL95++fPX0ANL1++1tXWBIqAFKA55fGTp/z8fGKiIrPnL+7smwwUASpggcjBgY+nG4Shq6OVnlOcm5HCy8vDBPQCRBQNSIiLAT3+7/8/oAImYHgAhf7+/fvr9+/vP35AVAClFy9bZWttwcfLC1QACoIr50939k64dv2mtJSkiaH+i5evXr15KyMlGRMZysfPr6ZtQFxgEo4WBgYA5PenFhKZDwsAAAAASUVORK5CYII=') no-repeat;
}
.mana-16 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAE7SURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpFGBkClimra7OwcTEAzbl+7uGv33prG9jdv30GkT54+O2XG3EOHjwI1AxUwAW359fMHEBw4dPT3799AFQuXrjx34XJWepK5qRHQeKACJqA7mJmZ3V2dIGbcuXt/5+79Lo52p06f/fXrF1AEqIAJzSk79+wHyl25duPQ0ROFZbUgF//4zgSVhIHXb94CTQ308yrITrt2/ea9+w+BgkxAL0CkIUBURPj7d5DZbGxs3Dzcf/78ASpgAoYHUOj3nz9A8vOXLx6uTsdPngFyX7x8xcHOrqSkAFTA+Pfvn/Onjk2cMv3y1esKcrIFOenXbtw8cerc379/Q4N81dXV1bQNiApMIqKFgQEAeKDC4O/kvu0AAAAASUVORK5CYII=') no-repeat;
}
.mana-17{
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEqSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpFGBkClimra7OwcjH///rl+6eyZs+d27NqXn5124tSZ3fsOcnNxQdQVFWSbWdkzAW358f3btes3t2zf9ffv3xOnzxrq69rZWALR23fv2FiYgQoYb1w+C1T0798/O1e/HRtXvnn3TkFOFmjG4aMnfvz44erswMHJxQRxChMTE9h4BogKoENXr9vkYG8DZAMVQOXQwOmzFyQlxVlZWCBcJqAXICxksHHLdlMjAwgbqIAJGB5A1sePn4DkBzAJdN/xk2c01FVBShgYgAqYgCH25+//hUtX2lpbrF638ffv358/f9HSVBMTFQGqABoDVEBcYBKOFgYGALrIn9uhneDtAAAAAElFTkSuQmCC') no-repeat;
}
.mana-18{
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFCSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpFGBkClimra7OwcLEAzgCquX79x+NgJDTUVc1Pjw0dPXLx89efPny5O9kClatoGTEBbgGa8ev16+ap1b9+9v3LtxqatO3IykjNTE9u7J3788B6ogAnoDqByCzMTFWVFIOPdu/fPn78Ekhwc7Dw83AyMjEAFLGhOsbEyP3HqTEh0soKcbE5mMgc7O1ABE1QSBj5++vT9x8+m2nIhIcGuvsk/fv4ECjIBvQCRhoD1m7Zpa6oDzetuq5eSlLh46QpQARMwPCDS3759//nzl7iY2M1bd4BcRkZGFhYWGWkpoALGv3//3Lp6YdbcBafPnufh5k6Kj7px4/bzly9ZWVj19bStLC2AQUBUYBIRLQwMAOBQr7aPizrUAAAAAElFTkSuQmCC') no-repeat;
}
.mana-19 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAE8SURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpFGBkClimra7OwcLEAzgCouX7ly7PhpQUF+fx/Pq9dubN25h+H/fx8vd6BSNW0DJqAtQDOYGJl27tn/9u37799/1DZ1ZKTEpyTG1ja2v3zxAqiACegOoHIVZUUdLQ0g4/zFS/z8fHy8vEKCAqoqSgcOHwMqYEJzChMT0/v3HyBsbi6uV69eg+yB8OHA2MiAk5Nz1txFq9ZuPH7yjJCQIFCQCegFiDQEsLKwzJ7Wp6GuaqCnw8LCbGdjCVTABAwPiPSPHz++//gBZPBwc1tamK7ftC0jNUFMVASogAUYYu/evNy0eeunz1++ff9x5tyFz5+/XLtxy9vTFegVoDFABUQFJhHRwsAAAEKMo0bborQ8AAAAAElFTkSuQmCC') no-repeat;
}
.mana-20 {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFjSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpFGBkClimra7OwcjH///rl19cKxYydmz1/87///1MQYU2PDHbv23Xvw8PmLlxmpiR4+AQwvnz3evmlNcGDAvp2bm+qqRUTFN6xe6uvtdeHU4RWL5xkYGAAVMAHd8fjJs8LcDCFBATdXxx8/fmzYvF1DQw1onbqayqtXb86ePc0EdIqZiSEPDzdQFGiLu4vj+/cfODnYgVxGRkagzmdPnzGBXAgGFy5duX7zVkVpPhMz848fPyGCTMxMvDw8TEAvADnXrt+8eOlKRXHer1+/uDg579y9DxQE+gPI1dbRYgKGx937Dzr7Jv/9+2/ewmUllQ3enq537z149PjJvv2HAvy8RcUkQUGwYsmiq9eugY1nYGVlDfTz+vjx04HDRyXExexsbdS0DYgLTMLRwsAAABnzwHeRF3aFAAAAAElFTkSuQmCC') no-repeat;
}
.mana-2-B {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF8SURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/Th8/vHrtej0dLRMjA6AoBACVyitrcHJxMwHNuH/r6vqNm3083abPXvD792+oEqDxf/9ePXPs79+/TEBbgFZEhwcLCvCLi4mysLBAVLCysr19/vT71y/PHtxlAroDKMTIyLhq7cbC3Ix///7BVYAVM7x+/pgJ4tLO3kk/f/2au2Dpq9dvgCpWLF+5Yt3mG7fvAqW+ffnCePH0EaDbb9+5B+QzMTFpqKvVN7YeO3X20+cv3FycRVkpVmYmTEAvAKVVVZSACKjixaOHv379CvLxADouPz3pyo1bXDw8TMDwACoCAog7WFlZJMXFzl28oqelceLseWUFeVFJWSZgiAENQ3ZpdGgAMzPT12/fgCq8PVylFJRBgfnj29crZ44BfQtRBATXbt7m5ub6x8AUGh0PDExotABDDBgeQN8C/QLkAt0BtAVoBjMzMwMDAwArqbIv/yKO/gAAAABJRU5ErkJggg==') no-repeat;
}
.mana-2-G {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF8SURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/Th8/vHrtej0dLRMjA6AoBACVyqio8XDyMgHNuH/r6vqNm3083abPXvD792+oEgaGz4zfdtzY8ufvHyagLUArosODBQX4xcVEWVhYICp+cf6/9Onqx+8frz6/zAR0B1CIkZFx1dqNhbkZ//79g6sAK2a4++Y2E8Slnb2Tfv76NXfB0lev33xj+b1i77ot83c8uvUEKPXx+wfGi6ePAN1++849IJ+JiUlWW76lr3vfmkNALjMLc35fpoK6HBPQC0C+qooSEAFVAG05seM0UJqbjxsovn/tIX5OASZgeAA5QABxB9BUeU05EyfDH99+RBQGG9rrK4uoMgFDDGgY3KVAH6Q2xIvJirKysQgI89s6W2lL6oIC8+uPL9uvbwb6FmIkEPz6+fvLhy+S4lJ+RgF8HPzQaAGGGDA8gL4F+gXIBboDaAvQDBZmFgYGBgCzOLYQy2aUMQAAAABJRU5ErkJggg==') no-repeat;
}
.mana-2-R {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF2SURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/Th8/vHrtej0dLRMjA6AoBACVqigqs/AIMAHNuH/r6vqNm3083abPXvD792+oEgYG3u/v/u2ZzfDnFxPQFqAV0eHBggL84mKiLCwsEBUi/7+L3Njz/9Obv9ePMgHdARRiZGRctXZjYW7Gv3//4CrAihn+3r/ABHFpZ++kn79+zV2w9NXrNyL/vr3at2rbpQcfv/8CSv3/9Jrx4ukjQLffvnMPyGdiYjJXFL++aeGn7792X3n05P2XhgALHTkxxhuXz/74/g1kLpItzz58ffD605S9F8X5uSZkhjEBwwNNxdxDV/VqlrRvPX375QcjOVFmRQMmYIgBwwPZpa8/gwy+9PiNpqSgpooSs6Y1KDD/fP34b/csoG8hin79+bv5wn0mRgZLA225kHxGXmFotABDDBgeQN8C/QLkMfKBbAGawcDCxsDAAACkY8S0HdMPswAAAABJRU5ErkJggg==') no-repeat;
}
.mana-2-U {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFmSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/Th8/vHrtej0dLRMjA6AoBACVyqvpcrKzMQHNuH/r6vqNm3083abPXvD792+oEgaGP2x8F158+/vvPxPQFqAV0eHBggL84mKiLCwsEBXMfOIvmYS+/fr7+MMPJqA7gEKMjIyr1m4szM349+8fXAVYMcPLzz+ZIC7t7J3089evuQuWvnr9Bqji2utfR3ZthSj6+vsv1PCQQD8gycTEJKOm9/Qvb0thkLquoY2bN0SWCegFIKWqogREagbmQFv2bFx9/+a1PRtXPbh1HSjFzcrMBAwPsGqEOy6ePAIkJaTlLp85AWSI87IzAUMMaBiySyVk5YHk04f3gB7nYmOWFeAABeb3n7+A4QH0LUQR0CvbVi0BMkJi4s2VxDhZmaHRAgwxYHgAfQv0C5ALdAfQFqAZzEyMDAwMAPOdnxQjjxd1AAAAAElFTkSuQmCC') no-repeat;
}
.mana-2-W {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF9SURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/Th8/vHrtej0dLRMjA6AoBACVqmhos7ByMAHNuH/r6vqNm3083abPXvD792+oEgYGXn7O//9fAq1hAtoCtCI6PFhQgF9cTJSFhQWiQkRcSFSc7f//33//fmQCugMoxMjIuGrtxsLcjH///sFVgBUz/Pv7mQni0s7eST9//Zq7YOmr12+ERQUZmL7s33/i1y+Q1UDDGC+ePgJ0++0794B8JiYmEzP97z/flJZ3vn37IS013M7WTFpagvHG5bM/vn8DmQu2hYPrd1JyxbHj54DG2NmZzp/byc8vxAQMD7gKoDs4ONjNzfUFBfnZ2dnkZKUuXLjGxMzLAgyxd29eAn0LcSkrK0t0lN/37z9kZaX+/vljb2/NzMwPCsw/v3/+//8C6ECIkUDw6dMXPj6eP38YObnkGBlZodEC8unfj0DfQpQCJYC2AM0AeoaBgQEAtzarPAuf1fsAAAAASUVORK5CYII=') no-repeat;
}
.mana-B-G {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGHSURBVChTY/z//z8DA8Pfv38f3L7+7uXzn9+/AblcPDyikrJSCsrMzMxALhMQf//2tb6y1NDc1j0ocvXGrf////v6+dODW1fPH933+dtHkCKgGQtnT9+x5wAHOxsjA+ORE6dv3L4LlACCH+x/d97c9ufvH+aUuOgjhw9rqiozMjFKiIt5uzq+ffdBWUGOUYjz5t8HP//8ZGVmZXr9/LGjjQUvL8+9B4+ePn/By8PtYm8NUQEx7+6b20zfvnxhZGRUkJORkZKUk5GSl5X5x8++/sTOLfN3PLr1BKjo4/cPjIe3bwC6FKIJCIBmdM+ctm/NISCbmYU5vy9TQV2OCehbiDQQQGw5seM0UJqbjxsosn/tIX5OASZgeCCrAAabvKaciZPhj28/IgqDDe31lUVUmYAhxsnNA3cp0H2pDfFisqKsbCwCwvy2zlbakrqgEP/y7dOOm1s/fgeFGwT8+vn7y4cvkuJSfkYBfBz80GgBhtjV55eBvgX6BcgFugNoC9AMFmYWBgYGAMLJqhxVea9iAAAAAElFTkSuQmCC') no-repeat;
}
.mana-B {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGPSURBVChTY/z//z8DA8Pfv3+fPbj7+vnjFy9eMDMziwKBpKyUgjKQDZRlACr69vXLqQM7i3IyxCUkeXj5gUhRQbEwK/30wV1AKaACJqAZV04frWpsf/r8xdev30D6GBhev3137PTZSTPmXD1zDKiABWjLhs3bTpw5z8fLw8bK+uv3b4i6qzduA5GOhpq4tDwT0B17Dx011NOOiwgWFRGGqBATFc5MjDE11Nu2ez9QAdO3L1+AhtcU5Xi5OLRUFzMyMgIVNZYX+no4VxfnPH/5CqiABaRPRGTJmg1y0lIfP31WVpR/9frN+ctXb929/+79ByEBAaACxrOH9zx/9uz46XNAdOrcxdjwIBZm5vnLVluYGFqZGQNtlJKWZgGGx9fPn9yd7ORkpICK3rx9x8nBAdSdnhAlLioCZAAVMAFDjJObB8j58vWbrLTksVNnDx47KSUh9uPnT6AgUAqogAkYptomVkAOJwe7oa62tZlxbmq8orzc71+/gYJAKaAC9GgB+gXIZWJllVVQgUYLAwMAd3S7M57KFFAAAAAASUVORK5CYII=') no-repeat;
}
.mana-B-P {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFxSURBVChTY/z//z8DA8Pfv3+fPbj7+vnjL58+Xbhy3cbKXFRSVkpBmZmZGSgLUvT929erZ459//oFyL/38PH1m3e83RyBbE5uHm0TK04ubiagGXAVv3//OXDkxItXbyDGAwWBUkAFjI/u3Hxw6ypQ6O79RxNmLuDkYH/15i0HO0dxdrKqkjxQXEFNmwnoDiDr379/vdPmZiZGBXi7WpkaVRdlTpq18MfPn0ApoAKmb19AFl2+dktCTFRLXYWNlfXNu/ey0pL6Oho3bt8DSgEVMAEpIPjz5w8LC8gjnz9/FRUWAjKAqn///g2WZGDi4uEBUprqKrfvPrj34BEPDzfQ1R8/fT574YqyghxQCqgA4fCbd+73TZsrIyX59PkLoBNjwgLsLE2B4kCHMwItOn90HyQIvv/4sWbTDmAQpMaGCfDzAUWAQWVo7cQEDFNQiHGDLOXk4HCxtxIXFYarAEoBFaBHC9Avl67esDA3QUQLAwMA8Y7AP7Vm5bMAAAAASUVORK5CYII=') no-repeat;
}
.mana-B-R {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGKSURBVChTY/z//z8DA8Pfv38f3L7+7uXzn9+/AblcPDyikrJSCsrMzMxALhMQf//2tb6y1NDc1j0ocvXGrf////v6+dODW1fPH933+9M7kCKgGQtnT9+x5wAHOxsjA+ORE6dv3L4LlAACwb9f/u+by/DnF3NKXPSRw4c1VZWZmJgkxEW9XR3fvvugrCAnxcEg8/gEw89vjCzsTK+fP3a0seDl5bn74OHT5y94ebhd7K2hKsDg7/0LTN++fGFkZFSQk5GRkpSTkZKXlZHiZPh0ctu2Sw8+fv8FVPT/02vGw9s3AF0K0QQEQDPu71378duvPVcfPXn/pSHAQkdOjAnoW6g8WAXQFls1aSMFsTAzNTYW5rmHrzLyiTIBwwNZBZAx99BVvZol7VtP3375wUhOlFnRgAkYYpzcPMguff0ZFJ6XHr/RlBTUVFFi1rQGhfjvz+//753z/9MbiKJff/5uvnCfiZHB0kBbLiSfkVcYGi3AEPt7/SjQt0C/AHlAdwBtAZrBwMLGwMAAABgmq3sFycQFAAAAAElFTkSuQmCC') no-repeat;
}
.mana-G {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF7SURBVChTY/z//z8DA8Ofv3+uPr9868WNy5euiEgIy0jJKIuoakvqsjCzAGVBij79+LjhzLqP3z7Ma1x85/I9NnZWK29zIXEh71APDx1vPg5+ht9/fs8/MEdUUkJASJiHlx8ZReSFrT63DKiACWjLs+dPv3/5/uf3H6DJyGDL/J2XL1wBKmC6+eL62umboMIMDEJigkBSw1gVwl3ev+b2y5tMz14+u3PxHpDPyMgoISf27tV7IPvLx29gNQwvHr568/EN05+ff5iYmYB8Ll5OMVkxiBzQdn5hPgj7zbM3LIL8QswsTP//M1u4m146dhVoHtC/cuoy9oE2QNef2XdeSUmJ8cLjc7vP7GRkYhQQ4b97+f7k0plA3dVzS0SlRSAmmciZMwFDTEFeHqgC6Luf339CJP7++Qth8HPyAxUwAA3/+P3D4qMLQrND9K0MRMTFhERERCTETRxMFh2eB5QCKkCJlkv3Lzx99pRXkIfhB5O1obWOlD4oWhgYALFwqFkPjXZZAAAAAElFTkSuQmCC') no-repeat;
}
.mana-G-P {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF4SURBVChTY/z//z8DA8Ofv3+uPr98983t91/f3bpw19zaVFlEVVtSl4WZBSgLUvTpx8dd17d9/P4RyH967/mDa4+sfcyBbH5OfjdNLz4OfiagGXAVf37/Obf/4tsX7yHGAwWBUkAFjBcenzvz6CRQ6MndZyv617NzsL1/9YGdky2qJERWVRoobiJnzuwa5/Tj9/d///7NrF4Qku0npSTBysbqm+KxrHetiZMBMwvz99/fmT5+/wBUfvfSA2FJIUVteRZWlo9vPonLiqrqKz248Rhs6QcmIAUEf//8YWZmBjK+ff4mIMYPZLCysfz59QcsycDEzykApBS05B/dfvL07nMuXs7///5/+fD1xpnbMipSQCmgAoTDH918srRnDdCiV0/eAJ3oGediaKcLFAc6nPH3n98bLq2GBMHP7z/3rT4MDAL/NE9eAR6wMfwBeqFMwDAFhhiQAxRi52Q3dTESEheAqwBKARWgRwvQL7cv3jOxMEJECwMDAOLavZeFAemOAAAAAElFTkSuQmCC') no-repeat;
}
.mana-G-U {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF6SURBVChTY/z//z8DA8Ofv3+uPr98983tj98/ALn8nALKIqrakroszCxALkjRpx8f1xxbuXb+Blk1GT0rHXZONqAEWCm/u6Y/LwcX4+8/vxcfWpDnXwKRMHUxii4Jg7BlBLX5uHTNZPmZgLbs2rCbkZGRmYWZi5fr9J5zH99+gqhgYdH69uvv4w8/mIDu4BXgtfQ05ebj0jJVjywMYWFlhqiAmPfy808moEvN3U1s/aw5uDjYONj0rLXV5c0+vhU6smsrRNHX33+Z/ZN9/zP85xXg0bfRVdZVVFcwZ2RQrc2I+ff3r4mtI1ARIyMDE9C3EB18QrzaKpasrFp7Nq6+f/Pano2rHty6DhTnZmVmAoYHRBHcHRdPHgGSEtJyl8+cADLEedmZgCEGDA9kl0rIygPJpw/vvXn5nIuNWVaAAxSYn39+u/LiF9C3EEU/f3zftmoJkBESE2+uJMbJygyNlr///gPDA+hboF+AXKA7gLYAzWBmYmRgYAAAmW+MFdMXf4cAAAAASUVORK5CYII=') no-repeat;
}
.mana-G-W {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGASURBVChTLZHBS8JQHMff9jadm9ucRaSyHTIzULMws9LwFBGUXuqQHbsXUtQ/0Km/ontJHRUPQhcjiIQsIuncIYNmhaJb67de7/Qevw/f3/f7fZRlWQghwzQeXu9f2i29+wFP2eUJDocivhiDGXjaUKenl+pnpdNLLRSILUYcnAMGf6i8Fs1xrMCABhC7+QMymFtOFPY3yV3zjGKrjZCLhi2ViypFUZjBvMjfVG/19w4QMV942q9a1sA0dRp8SIq4sJp0y8JkIrRV3ACaEETvx/ykwWlqZXYplwYrLoGLZ6LzEzMBVqjVrvv9AUAghvM76xayRI8bxsHYWHJ8ymvgveJxuXLF85xX8UiSm4a0RFYekjLRZFgZOTw6qdfvms3n81JZFAWKYmnog0DEB8c5U6m4oshOp0NT/Y3GI41FBhprvT1BWuKUZZntQq7b7amq3zSMbDaNsWyX2Rt8Qx9gkEjC6XS+wIphUC5eg3X/32InNXVIS1DbBxZBAyEaIfQLv3eOqz+RRXkAAAAASUVORK5CYII=') no-repeat;
}
.mana-Q {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEPSURBVChTY/gPBl+/fu3p6TUztxAUEgEiIAPIBQpCZEGK7t69a2BozMPLj4aAgkApoAJGoHJrG7s7d+4wgIGri4u9gz2Qe//+/cuXrwgJCR09cohZSEh43fr1EBVA4OnhXlNd5eXp+frNm4MHD7148YKHm5tp1erVUHlUADTsy5cvQAZQAdPt21CL4KClte3b9++ZGRkQLkgB0C9AN5pbWObm5QMZRsamQBJoEdC9EpLSQDZQAQPQtwKCwjdv3gSK7ty1KzAwGOgpoJP//fsnLiEFVARUwAAMDyBLTV1j27btQHVwsHXrNqA4EAEVoASBkaGhr6+PlLQ00OAZ02cAXaaiogIMAqICk4ho+f8fAP4Nu28tFPTdAAAAAElFTkSuQmCC') no-repeat;
}
.mana-R-G {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGBSURBVChTY/z//z8DEPz59ff60b/3L/z/9BrIY+QTZVY0YNa0ZmBhA3GBiv5/frult+rF8+cXHr0ucDOUE+YFaQMpFfnrksTJI8wENOP33nmizL81pYT0ZEUCJm1+8v4LRNFzPv6tt3f9+fuHCWjL/09vfvz+c/jW03uvPjIxMr748BWo4oWM8h6Ofx+/f7z6/DIT0B1AoYQ5u9o2n37w5tPff/9FeTkhKsDGMdx9c5sJ4lJbNWkg+fzj14nR9kw6epMuX98yf8ejW0+Agh+/f2D8sbiS4d9fIOff//8gu2SU85Zs2bfmEFCEmYU5vy9TQV2OCehbIB8IICqAtpzYcRoozc3HDRTcv/YQP6cAEzA8IIogKoABIq8pZ+Jk+OPbj4jCYEN7fWURVSZgiAHDA+5SRkbG1IZ4MVlRVjYWAWF+W2crbUldUGD++PJ2y+1dQN9CjASCXz9/f/nwRVJcys8ogI+DHxotwBADhgfQt0C/ALlAdwBtAZrBwszCwMAAABU4tE5ME7m/AAAAAElFTkSuQmCC') no-repeat;
}
.mana-R {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGQSURBVChTY/z//z8DEPz59ff60cdnD7198YSTlUVZRZVFyZBZ05qBhQ0oCVL0//PbN5unda3cZawgxszENGHX+ZefvlX6mMa627A6JzHyCjMBzfi9d17Ngi0P3342kBMF6rNRk1qc5t619czMTQfOz28HKmCuDXX69/CSg4aMj4GSCC/nhnN3l5+4ZaEsef/Np43n7vEw/bXRUmT8ubHv/4cXIGfBwL///3/8/ps8b/eeq49YmZiuTyth+f/pNUTu/uuPN56/lxbkufv644azdyPN1V9+/Hbp8Ztf71+xQFQsP3GzYNnBv//APmVgqPY16995/vKTN0B7Rfm4mBj5RP/8+1ez9hhcRYGboa+BIlCFlAB3ta/pm/+cTMyKBt9//fn4/RdEBRBsuXjfZ8ImIIOPkw1onriBNRMwxPjEpLSkhCAqgODOyw/ywryRFurRlhoLCiNYNG2YgGEKDLFZOSFAw6GqGBg0JYXirTWzApwEvDOAChDR8vPKocdnD///8laQm4NPTBroDGi0MDAAAN4or3qfztnlAAAAAElFTkSuQmCC') no-repeat;
}
.mana-R-P {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFzSURBVChTY/z//z8DEPz59ff60b/3L/z58OrQzadO5obMigbMmtYMLGxASZCi/5/f/t477/+nN0D+lafvTt17mWSrCZLjE2F1TmLkFWYCmgFX8evP3zWn7zx8+/kf2HigIFAKqIDxz6V9f87vAApdevI2f+lhbnaWJ+++cLOzTo21N5ATAYqzGHowAd0BZP399z9r0cGOUMtMRx1vfYUFKc6Fy498+/UHJHX/AtP/T6+BrGN3niuI8JoribOzsjz78FVVXMBWTfLM/VdAKaACJiAFBL/+/GNlBrHff/0hLcgDZHCwsvz88xcsycDEyCcKpMyUxC88enP5yVsBLnagd998/rH32mNdGWGgFFABwuHnHr7OXnxQRVzg7quPf//9q/Q2DjBSAooDHc74//fPX1smQoLgy8/fk/dcAgZBc6C5KC8n2BgRNp98JmCYgkKMD+RbHnbWCHNVOSFeuAqgFFABerQA/XLk1jNbE31EtDAwAADyV7v/4UsLTAAAAABJRU5ErkJggg==') no-repeat;
}
.mana-R-W {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGDSURBVChTY/z//z8DEPz59ff60b/3L/z/9BrIY+QTZVY0YNa0ZmBhA3GBiv5/frult+rF8+cXHr0ucDOUE+YFaQMpFWH1SGfk4GMCmvF77zxR5t+aUkJ6siIBkzY/ef8FoohBRukP03sGhn9MQFv+f3rz4/efw7ee3nv1kYmR8cWHr0AFjFpmDLr6/////vv3IxPQHUChhDm72jaffvDm099//0V5OSEqwKYx/Pv7mQniUls1aSD5/OPXidH28rbOryVk9+8/8evXb6Ag0DDGH4srgYpBOv7/B9r1V8P4MZ9oaXnn27cf0lLD7WzNpKUlmIC+BaoAAqAKoC3fldTLyruOHz9/5cqtNWt38PJyMzKyMgHDA6II4g4ODnZzc31BQX52djY5WakLF64xMfOyAEPs790zQN9CXMrKyhId5ff9+w9ZWam/f/7Y21szM/ODA/PH5z9M74AOhBgJBJ8+feHj4/nzh5GTSw5oHSxagI7/+xHoW4hSsDt4gWYAncrAwAAAt/eqrK7r7XkAAAAASUVORK5CYII=') no-repeat;
}
.mana-S {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAE4SURBVChTY/z//z8DA8OvX78e3r/z59eP3Xv2ArmuLs4sbBzyiipsbGxALkjRzx/fT5882tzeJyrE//HLN6AoPw/X63cfayuLTM2t2Tk4Gf/+/XPlwunde/a9//T96bNnsbExQEWLFi2WkZYW5ON0dXHSMTBlevbkUVh00ut3n+/cvRsaErJ/334gCgsNBXKBguExyUAFTJ8/vre1Nreyslq8aOHmzZulpKUlpaSADCAXKGhjZQZUwNhYX/Ps5dtv374JCAgwMjCUlZUBrevq6gJ658OHD1xcXFLiwkxAITyAkRGokYEpNDjw7+/vIcHBvT3dwIDYtHkzEAEZQC5Q8M+vb0AFTLz8gkeOnTp27FhsXLyvr++zp0+BCMgAcoGCQCmgAuxBsHjxEmkpKXgQEBeYhKOFgQEA/P7IQI/HNAMAAAAASUVORK5CYII=') no-repeat;
}
.mana-T {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEySURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRz58/7t+6CpFGBkClimra7OwcTEAzsKoAAqAgUAqogAloC0TFjx8/NmzePn3WfCD57t17oAiQAZQCKmC8cfnsj+/f/v79m5pddOv2XbARDCwsLM6OdidPn92ydikHJxcLxJgLF68AVehqa0aEBj549HjVmg07d+8Din/5+pWRkZEJrJPh67dvQFJfT8fe1kqQn//nz18Q8WfPXwBJFqAXgNYpKSoAOZcuXwXaa2piuHjeNLAaBmEhQaACJmB4ADky0pJ6OlqXrlw7e/6SpIQ4HLGxsQEVMAFDDKgWqC4zLRFofWtX/69fULuAACgFVIASmHMXLJWVlXZzdoCrgAQmEdHCwAAAflyrAjDm4ZIAAAAASUVORK5CYII=') no-repeat;
}
.mana-U-B {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGDSURBVChTY/z//z8DA8Pff/8ff/jx5N2XXwxMQC43K7M4L7usAAczEyOQCxL6/vvvqccfm1tar9+4AdQCRF9+/b379htQ8Nv3HyBFQDMuPPt85MD+VXOmXD9/BigEByzfP1w7ffjv379MQFu+/fq7YuZEoOjaBTPevHwOUcH39/OP++e/f/3y7MFdppeffwKFHty+ASQ19I3OHN4PVwFSy8Dw+vljpq+//wJZJraOQPL0ob1ahiZAFZvnT16xbvON23eBgt++fGHce/sN0KV/fv++cemchIyckghPV2X+sVNnP33+ws3FWZSVYmVmwgT0LVA5CyurjrE5UMXnW6d//foV5OPBwsKSn5505cYtLh4eJmB4ABUBAcQdrKwskuJi5y5e0dPSOHH2vLKCvKikLBMwxLjYmJFdGh0awMzM9PXbN6AKbw9XKQVlUIh/+/Hz2qlDQN9CFAHBtZu3ubm5/jEwhUbHc3Jxw6Ll719geAB9C/QLkAt0B9AWoBnMzMwMDAwA1ATEGG+vZB0AAAAASUVORK5CYII=') no-repeat;
}
.mana-U {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAE0SURBVChTY/z//z8DA8Pff/8ff/jx8vPPr7//ArncrMzivOyyAhzMTIxALhMQf//999Tjj3fffvvy6y9Qy+GdW4EMIBcoCJQCKQKaceHZ52+/QBwgWDFz4r7NayBsoCBQCqiACWgLXMXJA3tWzJr06tlTCBcIgFJABUxAd0D4H9+9ndxYBmQ8eXD31ME9EEEgACpggrgUCNYtnPnl00cIe3pb7eePHyBsoAKQwyFg/9b1UBYDg4yCEtBxUA7Q4UDfAqlvXz5/ev8OIgQEzn4hL589gbCBCpiA4QFksXNwsrKxQUSBYPvqpY/u3oKwgQqYgCHGxcbMzMLi4BUIEQWCm5fPW7t4AhlAKaACUIgDQwwYHu8/fZnb23L64F4GRkYH74DozCI+bg4DKV5OVmYiooWBAQBSlKd9GixjLgAAAABJRU5ErkJggg==') no-repeat;
}
.mana-U-P {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF6SURBVChTY/z//z8DA8Pff/8ff/jx8vPPzz9/XTp53NrOXpyXXVaAg5mJESjLBMTff/899fjj3bffvvz6++D2rWePHwIZQC5QECgFUgQ048Kzz99+gTi/f/06tH3Ty6dP/v/7B+QCBYFSQAWM999+A2oCCt27cW1KUyUHF9fr58+AZEFTt7KmDlBcWZiL8cSD90DD//37Wxjhl1HV9PH9u6tnT7qHRPZVFbXPXcHOycnDxsz0FWzr1bOnJGRkNQ2M2djY3r56KaOgrGdmefPyeaAUUAHI4UDw+/dvZhZWIOPzxw8iEpJABisb269fv8CSDEzcrMxASlPf6M61y/dvXuPh5we6+uP7t+ePHVZS1wJKARUgHH77ysWJ9WXSCkrPHt7/9+9fZEa+jZs3UBzk8D9//wHDAxIE3799Xb9w9stnjxMLqwSEhIEiXGzMZrL8TMAwNZDiBXKAQpxc3E4+QWKS0nAVQCmgAvRoAfrlyukT5lbWiGhhYAAAbkHMEz2QWQgAAAAASUVORK5CYII=') no-repeat;
}
.mana-U-R {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF3SURBVChTY/z//z8DA8Pff/8ff/jx5N2XXwxMQC43K7M4L7usAAczEyOQCxL6/vvvqccfm1tar9+4AdQCRF9+/b379htQ8NfXzyBFQDMuPPt85MD+VXOmXD9/BigEB8KfHjDsnsHw5xcT0JZvv/6umDkRKLp2wYw3L59DVMj+eCx/fvn/T2/+Xj/K9PLzT6DQg9s3gKSGvtGZw/vhKkBqgc69f4Hp6++/QJaJrSOQPH1or5ahicz3x1+3Tt126cHH77+Agv8/vWbce/sN0KV/fv++cemchIycIf+fRyt7Pn77tefqoyfvvzQEWOjIiTGeePAe6BeQuUi2PPvw9cHrT1P2XhTn55qQGcYEDA80FXMPXdWrWdK+9fTtlx+M5ESZFQ2YgCHGxcaM7NLXn78ByUuP32hKCmqqKDFrWoNC/Ne3Lwy7pgN9C1H068/fzRfuA4Pa0kBbLiSfkVcYGi3AEAOGB9C3QL8AeYx8IFuAZjCwsDEwMAAA6yrNGOnSx68AAAAASUVORK5CYII=') no-repeat;
}
.mana-W-B {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF9SURBVChTY/z//z8DCPz7+/fjv7+f////DeQwMrIyMfMyM/MzMDABuSAMlPjy+e6d2xcWLlr569cvkMD/X3//vP396/Hvn99Bev7///vx4+0njx8nJJUB+T7eTnGxgdLS4kD2p/e/H9x6YmjtxAS0hZnpX0PjpDt3Ht64ce/o0bMQBwBVXD9/4/vXL88e3GUCuoORkVFaRkJRUVZAgM/cXP/duw8QFUClQPD6+WPGnz/uAB0B5EyavMjIUOvN2/dOdg6T+ue8fffeQFdLQ1WZkZEJoQhoC9BIoBmZmRXHTp399PkLNxdnUVaKlZkJE9C3YFOB3gapuHTqCtCDQT4eLCws+elJV27c4uLhYQKGB0QRxB2srCyS4mLnLl7R09I4cfa8soK8qKQsEzDEgIYhuzQ6NICZmenrt29AFd4erlIKyqAQ//3r+8UTR4C+hSgCgms3b3Nzc/1jYAqNjufk4oZGy9+/f4HhAfTtty8gpUB3AG0BmsHMzMzAwAAAzY/G/vJiHAIAAAAASUVORK5CYII=') no-repeat;
}
.mana-W {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGLSURBVChTY/gPBX///Hn36+fDT5+ubVg/B8gAcoGCEDkmBgagut+3bx3/++ftj59fAgMz3n/4+P//LyD396/HQCmgAqCif39+P5swcc6lyze/ff3x6/fvvv75r1+/Ky5p6+icevPGCaACRqCxQE129pFXr922sTHh5uL8/Pmrnp7GjJnLtm6ZY2FuwMwizPTv72eggRvWz4iI8Pn08fPOXYePHT8HRMrKcjw8XF+/fgMqYNy5YwkjI2Nbx3QuLs5Dh04BNUCAk6Plrdv3RUWFtm6Zy2RsrFta3vHo0TM2VhaoPBh8+fKVn483Oytm756jjEDfvn37+vOXrwWFLR/efwQ6H2iwvr6mpYVBWWkaHx8PIyMbExMzr5AQ/7ev34F2mZnrA6WBFrGwME+fsSw5pRJoJFABEzMz/717z+cvWOPhbldZnvH27fvXr98unN+1f99SKSkxRkZWoAJQiP/79+vXzwc/f9wGookTG0PD/L5+uQFk37p1FCgFVIAeLevXzf7+7RZKtPz/DwBg2QLLp+LstgAAAABJRU5ErkJggg==') no-repeat;
}
.mana-W-P {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFpSURBVChTY/gPBX///Hn36+fDb19v7di+BMgAcoGCEDkmBgagut+/fz3+++ft//+/rl+/c//BYyADyAUKAqWACoCK/v35/QzC+fXr95q1Ox4+fPbv3z+IZqAUUAHT378fISouXb7p5pF0+szlDRv32NhGXrh4HaIOqIABaP3PH7e/fb1pampy6OCadWtnFxdnXr6829LK4v27S0ApoAImiDHHjp1TUJA2N9dn52B79vyVmqqCna3JmTNXIIaBHA4Ev37/ZmFlATI+vP8kLSUOZLCzs/34+QssycDEyMgKpMxM9S6cv3758i1+AV6gn9+8eb9333E9XXWgFFABIzA8gL4Fcs6du5qV06iqIn/n7qO/f/9WVqQHBrgCxZlZhBmBIQYPjy9fvk2esvjhw6fNTQWiokIQY1jZZIFuYmJhlYJYysPDFRnhIyMjCVcBlAIqADkcohxkLCObgoKMg70ZkAHkAgVBmhkYAPMk1o1WW4JjAAAAAElFTkSuQmCC') no-repeat;
}
.mana-W-U {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFwSURBVChTRVFNSwJRFH3vTZN9ma5SsiFsEwQRtshNi1YRtDJaVkar/kBBu+on2C9I2lQrN61ctEgoSMqEtBq1UosBv1KrmdGZ6UwTduE97rnvcO6951HDMIgZuqZ96FrDMFoAlPKMs3OcgxAGaB48NBsZ8en2MHysqqpZMFStXW6pebXdBqELGvV6tpDPr29sAxeL0tpqwONxIa/Ktky1OSM4GLpwTN/dC4niSzqdjcXi1gA1ZSAp8V+qlq/JDHNQSj0jbq9XcDoH/f6pSqVmMUBFSA2FKrKIIQBCB+Fp30SpXJ1bCJzflVOJ+Oz8IuqUkn8SukASGjcFshVcGp/0be7sWySGbZH9ApOBLtHIae7hPho5eX5Mod7Pcwx+WKTOHImrC9yuYSF5fWkmdhuDYxDrMBBuYRT322uuJL33dXOCs8d0HMbFC01sa5EU+fvs5AjJ8krQPzbUy3N/36LpBvzAtp8tk4o50AUaHKOEkB8/eLy7BZycwQAAAABJRU5ErkJggg==') no-repeat;
}
.mana-X {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url('data:image/png; base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFdSURBVChTY/z//z8DA8O/f3/fvHz+/u2rnz++A7nsHJyCwmIi4pJMTMxALkjRt69fTh49+PPnDyCfl4eHi5vr5ctXQDYXF7e5jQM7OwfD379/zp48nBgfy8PLr6SkvGzhnNPH9nt7eYqIiFWVl1y/dAaogAVoCzMjQ352moS42Oz5Szg5OVlZWORkpYP8vY0N9YG2gxSkJMT8+fMbaLiOlsbzFy8XLVvFwswsLiZqY2UBFASCP79/MUFcCgFF+ZlCggIr1mxwtLeBCjEwABUwQZlg8O7deyVF+d+/f0+cNhsqBAZMQN9CWF++fp27YGledlp7U83W7bs3bd0JEQcqYAKGB0jFl69Vda0OdlYc7OzqaiqRYUF9k6YfOXYSKAVUwAQMMaDaWfMWAx177MRpoF0vXr56//69q5P9wcPHbt65B1QACkxgMN6/dRXZBxAA1Kyopg0MTCKihYEBAH+uo0BN0hOvAAAAAElFTkSuQmCC') no-repeat;
}
.mana-C {
display:inline-block;
width:12px;
height:12px;
\#padding-left:12px;
background: url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAMAAwDAREAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAACQP/xAAfEAACAwEBAQEAAwAAAAAAAAACAwQFBgEHCBQSFyH/xAAYAQACAwAAAAAAAAAAAAAAAAAABAIDBf/EACURAAIBAwMEAgMAAAAAAAAAAAECEQMSIQAxUQQTQWEjoTJSgf/aAAwDAQACEQMRAD8ATr7G+yvddx7tsMfjNZnvGP6Pz5e031t7Q4qWh9WzdUzvAwWAEyMI+IUBnE3XoEbrbGiuGU8ZNTI7IawkCbyWa5rjaoXNk8j9hiB963FUUgKdMKgQXMzR8nqdyreT/ANJ78ofcGz9B8LxeyfhtdTBfw/3xabfwGw9JVRTBXAiN/kRMl15GLJNRYM4sptbIiv4lIkKxkKr05Qy8HDLBkb75zqt+lpVj3I7dwyu2eYjz96l9x/JXhHom4wE7ZYqLelQa6HvqNEwgKNV6GOwun2OAqE2Vc1nQdY0stkmrmuRGN0Uvzp4BWUIxtkXmCPGRuPed9HSua1IdyGNP8SRnG08++caSDy/y/F57F1EWLUJl9lpCykybIFSpTpUpSuH3p8UoAWAKWtSlrAAAOf50ukRM06aooAHsk5JJ0hXr1XqsSxEEqApKiFJjE78nX//2Q==) no-repeat;
}
</style></head><body>"""

View file

@ -15,9 +15,14 @@ def mtg_open_json(fname, verbose = False):
for k_set in jobj: for k_set in jobj:
set = jobj[k_set] set = jobj[k_set]
setname = set['name'] setname = set['name']
if 'magicCardsInfoCode' in set:
codename = set['magicCardsInfoCode']
else:
codename = ''
for card in set['cards']: for card in set['cards']:
card[utils.json_field_set_name] = setname card[utils.json_field_set_name] = setname
card[utils.json_field_info_code] = codename
cardnumber = None cardnumber = None
if 'number' in card: if 'number' in card:
@ -137,9 +142,10 @@ def mtg_open_file(fname, verbose = False,
for card_src in text.split(utils.cardsep): for card_src in text.split(utils.cardsep):
if card_src: if card_src:
card = cardlib.Card(card_src, fmt_ordered=fmt_ordered) card = cardlib.Card(card_src, fmt_ordered=fmt_ordered)
# unlike opening from json, we still want to return invalid cards
cards += [card]
if card.valid: if card.valid:
valid += 1 valid += 1
cards += [card]
elif card.parsed: elif card.parsed:
invalid += 1 invalid += 1
else: else:

View file

@ -13,7 +13,7 @@ class Manacost:
colors = '' colors = ''
for sym in self.symbols: for sym in self.symbols:
if self.symbols[sym] > 0: if self.symbols[sym] > 0:
symcolors = re.sub(r'2|P|S|X', '', sym) symcolors = re.sub(r'2|P|S|X|C', '', sym)
for symcolor in symcolors: for symcolor in symcolors:
if symcolor not in colors: if symcolor not in colors:
colors += symcolor colors += symcolor
@ -136,7 +136,7 @@ class Manacost:
else: else:
ld = '' ld = ''
rd = '' rd = ''
return ' '.join(map(lambda s: ld + s + rd, self.sequence)) return ' '.join(map(lambda s: ld + s + rd, sorted(self.sequence)))
class Manatext: class Manatext:
@ -181,7 +181,9 @@ class Manatext:
def format(self, for_forum = False, for_html = False): def format(self, for_forum = False, for_html = False):
text = self.text text = self.text
for cost in self.costs: for cost in self.costs:
text = text.replace(utils.reserved_mana_marker, cost.format(for_forum = for_forum, for_html = for_html), 1) text = text.replace(utils.reserved_mana_marker, cost.format(for_forum=for_forum, for_html=for_html), 1)
if for_html:
text = text.replace('\n', '<br>\n')
return text return text
def encode(self, randomize = False): def encode(self, randomize = False):

View file

@ -1,16 +1,62 @@
# This module is misleadingly named, as it has other utilities as well
# that are generally necessary when trying to postprocess output by
# comparing it against existing cards.
import difflib import difflib
import os import os
import multiprocessing
import utils
import jdecode import jdecode
import cardlib import cardlib
libdir = os.path.dirname(os.path.realpath(__file__)) libdir = os.path.dirname(os.path.realpath(__file__))
datadir = os.path.realpath(os.path.join(libdir, '../data')) datadir = os.path.realpath(os.path.join(libdir, '../data'))
# multithreading control parameters
cores = multiprocessing.cpu_count()
# split a list into n pieces; return a list of these lists
# has slightly interesting behavior, in that if n is large, it can
# run out of elements early and return less than n lists
def list_split(l, n):
if n <= 0:
return l
split_size = len(l) / n
if len(l) % n > 0:
split_size += 1
return [l[i:i+split_size] for i in range(0, len(l), split_size)]
# flatten a list of lists into a single list of all their contents, in order
def list_flatten(l):
return [item for sublist in l for item in sublist]
# isolated logic for multiprocessing
def f_nearest(name, matchers, n):
for m in matchers:
m.set_seq1(name)
ratios = [(m.ratio(), m.b) for m in matchers]
ratios.sort(reverse = True)
if ratios[0][0] >= 1:
return ratios[:1]
else:
return ratios[:n]
def f_nearest_per_thread(workitem):
(worknames, names, n) = workitem
# each thread (well, process) needs to generate its own matchers
matchers = [difflib.SequenceMatcher(b=name, autojunk=False) for name in names]
return map(lambda name: f_nearest(name, matchers, n), worknames)
class Namediff: class Namediff:
def __init__(self, verbose = True, def __init__(self, verbose = True,
json_fname = os.path.join(datadir, 'AllSets.json')): json_fname = os.path.join(datadir, 'AllSets.json')):
self.verbose = verbose self.verbose = verbose
self.names = {} self.names = {}
self.codes = {}
self.cardstrings = {}
if self.verbose: if self.verbose:
print 'Setting up namediff...' print 'Setting up namediff...'
@ -28,30 +74,47 @@ class Namediff:
card = cardlib.Card(jcards[idx]) card = cardlib.Card(jcards[idx])
name = card.name name = card.name
jname = jcards[idx]['name'] jname = jcards[idx]['name']
jcode = jcards[idx][utils.json_field_info_code]
if 'number' in jcards[idx]:
jnum = jcards[idx]['number']
else:
jnum = ''
if name in self.names: if name in self.names:
print ' Duplicate name ' + name + ', ignoring.' print ' Duplicate name ' + name + ', ignoring.'
else: else:
self.names[name] = jname self.names[name] = jname
self.cardstrings[name] = card.encode()
if jcode and jnum:
self.codes[name] = jcode + '/' + jnum + '.jpg'
else:
self.codes[name] = ''
namecount += 1 namecount += 1
print ' Read ' + str(namecount) + ' unique cardnames' print ' Read ' + str(namecount) + ' unique cardnames'
print ' Building SequenceMatcher objects.' print ' Building SequenceMatcher objects.'
self.matchers = [difflib.SequenceMatcher(b=n, autojunk=False) for n in self.names] self.matchers = [difflib.SequenceMatcher(b=n, autojunk=False) for n in self.names]
self.card_matchers = [difflib.SequenceMatcher(b=self.cardstrings[n], autojunk=False) for n in self.cardstrings]
print '... Done.' print '... Done.'
def nearest(self, name, n=3): def nearest(self, name, n=3):
for m in self.matchers: return f_nearest(name, self.matchers, n)
m.set_seq1(name)
ratios = [(m.ratio(), m.b) for m in self.matchers]
ratios.sort(reverse = True)
if ratios[0][0] >= 1:
return ratios[:1]
else:
return ratios[:n]
def nearest_par(self, names, n=3, threads=cores):
workpool = multiprocessing.Pool(threads)
proto_worklist = list_split(names, threads)
worklist = map(lambda x: (x, self.names, n), proto_worklist)
donelist = workpool.map(f_nearest_per_thread, worklist)
return list_flatten(donelist)
def nearest_card(self, card, n=5):
return f_nearest(card.encode(), self.card_matchers, n)
def nearest_card_par(self, cards, n=5, threads=cores):
workpool = multiprocessing.Pool(threads)
proto_worklist = list_split(cards, threads)
worklist = map(lambda x: (map(lambda c: c.encode(), x), self.cardstrings.values(), n), proto_worklist)
donelist = workpool.map(f_nearest_per_thread, worklist)
return list_flatten(donelist)

305
lib/nltk_model.py Normal file
View file

@ -0,0 +1,305 @@
# Natural Language Toolkit: Language Models
#
# Copyright (C) 2001-2014 NLTK Project
# Authors: Steven Bird <stevenbird1@gmail.com>
# Daniel Blanchard <dblanchard@ets.org>
# Ilia Kurenkov <ilia.kurenkov@gmail.com>
# URL: <http://nltk.org/>
# For license information, see LICENSE.TXT
#
# adapted for mtgencode Nov. 2015
# an attempt was made to preserve the exact functionality of this code,
# hampered somewhat by its brokenness
from __future__ import unicode_literals
from math import log
from nltk.probability import ConditionalProbDist, ConditionalFreqDist, LidstoneProbDist
from nltk.util import ngrams
from nltk_model_api import ModelI
from nltk import compat
def _estimator(fdist, **estimator_kwargs):
"""
Default estimator function using a LidstoneProbDist.
"""
# can't be an instance method of NgramModel as they
# can't be pickled either.
return LidstoneProbDist(fdist, 0.001, **estimator_kwargs)
@compat.python_2_unicode_compatible
class NgramModel(ModelI):
"""
A processing interface for assigning a probability to the next word.
"""
def __init__(self, n, train, pad_left=True, pad_right=False,
estimator=None, **estimator_kwargs):
"""
Create an ngram language model to capture patterns in n consecutive
words of training text. An estimator smooths the probabilities derived
from the text and may allow generation of ngrams not seen during
training. See model.doctest for more detailed testing
>>> from nltk.corpus import brown
>>> lm = NgramModel(3, brown.words(categories='news'))
>>> lm
<NgramModel with 91603 3-grams>
>>> lm._backoff
<NgramModel with 62888 2-grams>
>>> lm.entropy(brown.words(categories='humor'))
... # doctest: +ELLIPSIS
12.0399...
:param n: the order of the language model (ngram size)
:type n: int
:param train: the training text
:type train: list(str) or list(list(str))
:param pad_left: whether to pad the left of each sentence with an (n-1)-gram of empty strings
:type pad_left: bool
:param pad_right: whether to pad the right of each sentence with an (n-1)-gram of empty strings
:type pad_right: bool
:param estimator: a function for generating a probability distribution
:type estimator: a function that takes a ConditionalFreqDist and
returns a ConditionalProbDist
:param estimator_kwargs: Extra keyword arguments for the estimator
:type estimator_kwargs: (any)
"""
# protection from cryptic behavior for calling programs
# that use the pre-2.0.2 interface
assert(isinstance(pad_left, bool))
assert(isinstance(pad_right, bool))
self._lpad = ('',) * (n - 1) if pad_left else ()
self._rpad = ('',) * (n - 1) if pad_right else ()
# make sure n is greater than zero, otherwise print it
assert (n > 0), n
# For explicitness save the check whether this is a unigram model
self.is_unigram_model = (n == 1)
# save the ngram order number
self._n = n
# save left and right padding
self._lpad = ('',) * (n - 1) if pad_left else ()
self._rpad = ('',) * (n - 1) if pad_right else ()
if estimator is None:
estimator = _estimator
cfd = ConditionalFreqDist()
# set read-only ngrams set (see property declaration below to reconfigure)
self._ngrams = set()
# If given a list of strings instead of a list of lists, create enclosing list
if (train is not None) and isinstance(train[0], compat.string_types):
train = [train]
# we need to keep track of the number of word types we encounter
vocabulary = set()
for sent in train:
raw_ngrams = ngrams(sent, n, pad_left, pad_right, pad_symbol='')
for ngram in raw_ngrams:
self._ngrams.add(ngram)
context = tuple(ngram[:-1])
token = ngram[-1]
cfd[context][token] += 1
vocabulary.add(token)
# Unless number of bins is explicitly passed, we should use the number
# of word types encountered during training as the bins value.
# If right padding is on, this includes the padding symbol.
if 'bins' not in estimator_kwargs:
estimator_kwargs['bins'] = len(vocabulary)
self._model = ConditionalProbDist(cfd, estimator, **estimator_kwargs)
# recursively construct the lower-order models
if not self.is_unigram_model:
self._backoff = NgramModel(n-1, train,
pad_left, pad_right,
estimator,
**estimator_kwargs)
self._backoff_alphas = dict()
# For each condition (or context)
for ctxt in cfd.conditions():
backoff_ctxt = ctxt[1:]
backoff_total_pr = 0.0
total_observed_pr = 0.0
# this is the subset of words that we OBSERVED following
# this context.
# i.e. Count(word | context) > 0
for words in self._words_following(ctxt, cfd):
# so, _words_following as fixed gives back a whole list now...
for word in words:
total_observed_pr += self.prob(word, ctxt)
# we also need the total (n-1)-gram probability of
# words observed in this n-gram context
backoff_total_pr += self._backoff.prob(word, backoff_ctxt)
assert (0 <= total_observed_pr <= 1), total_observed_pr
# beta is the remaining probability weight after we factor out
# the probability of observed words.
# As a sanity check, both total_observed_pr and backoff_total_pr
# must be GE 0, since probabilities are never negative
beta = 1.0 - total_observed_pr
# backoff total has to be less than one, otherwise we get
# an error when we try subtracting it from 1 in the denominator
assert (0 <= backoff_total_pr < 1), backoff_total_pr
alpha_ctxt = beta / (1.0 - backoff_total_pr)
self._backoff_alphas[ctxt] = alpha_ctxt
# broken
# def _words_following(self, context, cond_freq_dist):
# for ctxt, word in cond_freq_dist.iterkeys():
# if ctxt == context:
# yield word
# fixed
def _words_following(self, context, cond_freq_dist):
for ctxt in cond_freq_dist.iterkeys():
if ctxt == context:
yield cond_freq_dist[ctxt].keys()
def prob(self, word, context):
"""
Evaluate the probability of this word in this context using Katz Backoff.
:param word: the word to get the probability of
:type word: str
:param context: the context the word is in
:type context: list(str)
"""
context = tuple(context)
if (context + (word,) in self._ngrams) or (self.is_unigram_model):
return self._model[context].prob(word)
else:
return self._alpha(context) * self._backoff.prob(word, context[1:])
def _alpha(self, context):
"""Get the backoff alpha value for the given context
"""
error_message = "Alphas and backoff are not defined for unigram models"
assert not self.is_unigram_model, error_message
if context in self._backoff_alphas:
return self._backoff_alphas[context]
else:
return 1
def logprob(self, word, context):
"""
Evaluate the (negative) log probability of this word in this context.
:param word: the word to get the probability of
:type word: str
:param context: the context the word is in
:type context: list(str)
"""
return -log(self.prob(word, context), 2)
@property
def ngrams(self):
return self._ngrams
@property
def backoff(self):
return self._backoff
@property
def model(self):
return self._model
def choose_random_word(self, context):
'''
Randomly select a word that is likely to appear in this context.
:param context: the context the word is in
:type context: list(str)
'''
return self.generate(1, context)[-1]
# NB, this will always start with same word if the model
# was trained on a single text
def generate(self, num_words, context=()):
'''
Generate random text based on the language model.
:param num_words: number of words to generate
:type num_words: int
:param context: initial words in generated string
:type context: list(str)
'''
text = list(context)
for i in range(num_words):
text.append(self._generate_one(text))
return text
def _generate_one(self, context):
context = (self._lpad + tuple(context))[-self._n + 1:]
if context in self:
return self[context].generate()
elif self._n > 1:
return self._backoff._generate_one(context[1:])
else:
return '.'
def entropy(self, text):
"""
Calculate the approximate cross-entropy of the n-gram model for a
given evaluation text.
This is the average log probability of each word in the text.
:param text: words to use for evaluation
:type text: list(str)
"""
H = 0.0 # entropy is conventionally denoted by "H"
text = list(self._lpad) + text + list(self._rpad)
for i in range(self._n - 1, len(text)):
context = tuple(text[(i - self._n + 1):i])
token = text[i]
H += self.logprob(token, context)
return H / float(len(text) - (self._n - 1))
def perplexity(self, text):
"""
Calculates the perplexity of the given text.
This is simply 2 ** cross-entropy for the text.
:param text: words to calculate perplexity of
:type text: list(str)
"""
return pow(2.0, self.entropy(text))
def __contains__(self, item):
if not isinstance(item, tuple):
item = (item,)
return item in self._model
def __getitem__(self, item):
if not isinstance(item, tuple):
item = (item,)
return self._model[item]
def __repr__(self):
return '<NgramModel with %d %d-grams>' % (len(self._ngrams), self._n)
if __name__ == "__main__":
import doctest
doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE)

42
lib/nltk_model_api.py Normal file
View file

@ -0,0 +1,42 @@
# Natural Language Toolkit: API for Language Models
#
# Copyright (C) 2001-2014 NLTK Project
# Author: Steven Bird <stevenbird1@gmail.com>
# URL: <http://nltk.org/>
# For license information, see LICENSE.TXT
#
# imported for use in mtgcode Nov. 2015
# should this be a subclass of ConditionalProbDistI?
class ModelI(object):
"""
A processing interface for assigning a probability to the next word.
"""
def __init__(self):
'''Create a new language model.'''
raise NotImplementedError()
def prob(self, word, context):
'''Evaluate the probability of this word in this context.'''
raise NotImplementedError()
def logprob(self, word, context):
'''Evaluate the (negative) log probability of this word in this context.'''
raise NotImplementedError()
def choose_random_word(self, context):
'''Randomly select a word that is likely to appear in this context.'''
raise NotImplementedError()
def generate(self, n):
'''Generate n words of text from the language model.'''
raise NotImplementedError()
def entropy(self, text):
'''Evaluate the total entropy of a message with respect to the model.
This is the sum of the log probability of each word in the message.'''
raise NotImplementedError()

View file

@ -1,5 +1,6 @@
# transform passes used to encode / decode cards # transform passes used to encode / decode cards
import re import re
import random
# These could probably use a little love... They tend to hardcode in lots # These could probably use a little love... They tend to hardcode in lots
# of things very specific to the mtgjson format. # of things very specific to the mtgjson format.
@ -94,6 +95,19 @@ def text_pass_2_cardname(s, name):
for override in overrides: for override in overrides:
s = s.replace(override, this_marker) s = s.replace(override, this_marker)
# stupid planeswalker abilities
s = s.replace('to him.', 'to ' + this_marker + '.')
s = s.replace('to him this', 'to ' + this_marker + ' this')
s = s.replace('to himself', 'to itself')
s = s.replace("he's", this_marker + ' is')
# sometimes we actually don't want to do this replacement
s = s.replace('named ' + this_marker, 'named ' + name)
s = s.replace('name is still ' + this_marker, 'name is still ' + name)
s = s.replace('named keeper of ' + this_marker, 'named keeper of ' + name)
s = s.replace('named kobolds of ' + this_marker, 'named kobolds of ' + name)
s = s.replace('named sword of kaldra, ' + this_marker, 'named sword of kaldra, ' + name)
return s return s
@ -133,7 +147,14 @@ def text_pass_4b_x(s):
s = s.replace(u'x\u2014', x_marker + u'\u2014') s = s.replace(u'x\u2014', x_marker + u'\u2014')
s = s.replace('x.', x_marker + '.') s = s.replace('x.', x_marker + '.')
s = s.replace('x,', x_marker + ',') s = s.replace('x,', x_marker + ',')
s = s.replace('x is', x_marker + ' is')
s = s.replace('x can\'t', x_marker + ' can\'t')
s = s.replace('x/x', x_marker + '/' + x_marker) s = s.replace('x/x', x_marker + '/' + x_marker)
s = s.replace('x target', x_marker + ' target')
s = s.replace('si' + x_marker + ' target', 'six target')
s = s.replace('avara' + x_marker, 'avarax')
# there's also some stupid ice age card that wants -x/-y
s = s.replace('/~', '/-')
return s return s
@ -249,6 +270,7 @@ def text_pass_5_counters(s):
'petrification counter', 'petrification counter',
'shred counter', 'shred counter',
'pupa counter', 'pupa counter',
'crystal counter',
] ]
usedcounters = [] usedcounters = []
for countername in allcounters: for countername in allcounters:
@ -336,11 +358,11 @@ def text_pass_7_choice(s):
# the idea is to take 'choose n ~\n=ability\n=ability\n' # the idea is to take 'choose n ~\n=ability\n=ability\n'
# to '[n = ability = ability]\n' # to '[n = ability = ability]\n'
def choice_formatting_helper(s_helper, prefix, count): def choice_formatting_helper(s_helper, prefix, count, suffix = ''):
single_choices = re.findall(ur'(' + prefix + ur'\n?(\u2022.*(\n|$))+)', s_helper) single_choices = re.findall(ur'(' + prefix + ur'\n?(\u2022.*(\n|$))+)', s_helper)
for choice in single_choices: for choice in single_choices:
newchoice = choice[0] newchoice = choice[0]
newchoice = newchoice.replace(prefix, unary_marker + (unary_counter * count)) newchoice = newchoice.replace(prefix, unary_marker + (unary_counter * count) + suffix)
newchoice = newchoice.replace('\n', ' ') newchoice = newchoice.replace('\n', ' ')
if newchoice[-1:] == ' ': if newchoice[-1:] == ' ':
newchoice = choice_open_delimiter + newchoice[:-1] + choice_close_delimiter + '\n' newchoice = choice_open_delimiter + newchoice[:-1] + choice_close_delimiter + '\n'
@ -358,6 +380,12 @@ def text_pass_7_choice(s):
s = choice_formatting_helper(s, ur'choose khans or dragons.', 1) s = choice_formatting_helper(s, ur'choose khans or dragons.', 1)
# this is for 'an opponent chooses one', which will be a bit weird but still work out # this is for 'an opponent chooses one', which will be a bit weird but still work out
s = choice_formatting_helper(s, ur'chooses one \u2014', 1) s = choice_formatting_helper(s, ur'chooses one \u2014', 1)
# Demonic Pact has 'choose one that hasn't been chosen'...
s = choice_formatting_helper(s, ur"choose one that hasn't been chosen \u2014", 1,
suffix=" that hasn't been chosen")
# 'choose n. you may choose the same mode more than once.'
s = choice_formatting_helper(s, ur'choose three. you may choose the same mode more than once.', 3,
suffix='. you may choose the same mode more than once.')
return s return s
@ -423,31 +451,138 @@ def text_pass_11_linetrans(s):
lines = s.split(utils.newline) lines = s.split(utils.newline)
for line in lines: for line in lines:
line = line.strip()
if line == '':
continue
if not '.' in line: if not '.' in line:
# because this is inconsistent # because this is inconsistent
line = line.replace(';', ',') line = line.replace(',', ';')
sublines = line.split(',') line = line.replace('; where', ', where') # Thromok the Insatiable
line = line.replace('; and', ', and') # wonky protection
line = line.replace('; from', ', from') # wonky protection
line = line.replace('upkeep;', 'upkeep,') # wonky protection
sublines = line.split(';')
for subline in sublines: for subline in sublines:
subline = subline.strip()
if 'equip' in subline or 'enchant' in subline: if 'equip' in subline or 'enchant' in subline:
prelines += [subline.strip()] prelines += [subline]
elif 'countertype' in subline or 'kicker' in subline: elif 'countertype' in subline or 'kicker' in subline:
postlines += [subline.strip()] postlines += [subline]
else: else:
keylines += [subline.strip()] keylines += [subline]
elif u'\u2014' in line and not u' \u2014 ' in line: elif u'\u2014' in line and not u' \u2014 ' in line:
if 'equip' in line or 'enchant' in line: if 'equip' in line or 'enchant' in line:
prelines += [line.strip()] prelines += [line]
elif 'countertype' in line or 'kicker' in line: elif 'countertype' in line or 'kicker' in line:
postlines += [line.strip()] postlines += [line]
else: else:
keylines += [line.strip()] keylines += [line]
else: else:
mainlines += [line.strip()] mainlines += [line]
alllines = prelines + keylines + mainlines + postlines alllines = prelines + keylines + mainlines + postlines
return utils.newline.join(alllines) return utils.newline.join(alllines)
# randomize the order of the lines
# not a text pass, intended to be invoked dynamically when encoding a card
# call this on fully encoded text, with mana symbols expanded
def separate_lines(text):
# forget about level up, ignore empty text too while we're at it
if text == '' or 'level up' in text:
return [],[],[],[],[]
preline_search = ['equip', 'fortify', 'enchant ', 'bestow']
# probably could use optimization with a regex
costline_search = [
'multikicker', 'kicker', 'suspend', 'echo', 'awaken',
'buyback', 'dash', 'entwine', 'evoke', 'flashback',
'madness', 'megamorph', 'morph', 'miracle', 'ninjutsu', 'overload',
'prowl', 'recover', 'reinforce', 'replicate', 'scavenge', 'splice',
'surge', 'unearth', 'transmute', 'transfigure',
]
# cycling is a special case to handle the variants
postline_search = ['countertype']
keyline_search = ['cumulative']
prelines = []
keylines = []
mainlines = []
costlines = []
postlines = []
lines = text.split(utils.newline)
# we've already done linetrans once, so some of the irregularities have been simplified
for line in lines:
if not '.' in line:
if any(line.startswith(s) for s in preline_search):
prelines.append(line)
elif any(line.startswith(s) for s in postline_search):
postlines.append(line)
elif any(line.startswith(s) for s in costline_search) or 'cycling' in line:
costlines.append(line)
else:
keylines.append(line)
elif (utils.dash_marker in line and not
(' '+utils.dash_marker+' ' in line or 'non'+utils.dash_marker in line)):
if any(line.startswith(s) for s in preline_search):
prelines.append(line)
elif any(line.startswith(s) for s in costline_search) or 'cycling' in line:
costlines.append(line)
elif any(line.startswith(s) for s in keyline_search):
keylines.append(line)
else:
mainlines.append(line)
elif ': monstrosity' in line:
costlines.append(line)
else:
mainlines.append(line)
return prelines, keylines, mainlines, costlines, postlines
choice_re = re.compile(re.escape(utils.choice_open_delimiter) + r'.*' +
re.escape(utils.choice_close_delimiter))
choice_divider = ' ' + utils.bullet_marker + ' '
def randomize_choice(line):
choices = re.findall(choice_re, line)
if len(choices) < 1:
return line
new_line = line
for choice in choices:
parts = choice[1:-1].split(choice_divider)
if len(parts) < 3:
continue
choiceparts = parts[1:]
random.shuffle(choiceparts)
new_line = new_line.replace(choice,
utils.choice_open_delimiter +
choice_divider.join(parts[:1] + choiceparts) +
utils.choice_close_delimiter,
1)
return new_line
def randomize_lines(text):
if text == '' or 'level up' in text:
return text
prelines, keylines, mainlines, costlines, postlines = separate_lines(text)
random.shuffle(prelines)
random.shuffle(keylines)
new_mainlines = []
for line in mainlines:
if line.endswith(utils.choice_close_delimiter):
new_mainlines.append(randomize_choice(line))
# elif utils.choice_open_delimiter in line or utils.choice_close_delimiter in line:
# print(line)
else:
new_mainlines.append(line)
random.shuffle(new_mainlines)
random.shuffle(costlines)
#random.shuffle(postlines) # only one kind ever (countertype)
return utils.newline.join(prelines+keylines+new_mainlines+costlines+postlines)
# Text unpasses, for decoding. All assume the text inside a Manatext, so don't do anything # Text unpasses, for decoding. All assume the text inside a Manatext, so don't do anything
# weird with the mana cost symbol. # weird with the mana cost symbol.

File diff suppressed because one or more lines are too long

351
mtg_sweep1.ipynb Normal file

File diff suppressed because one or more lines are too long

167
scripts/analysis.py Executable file
View file

@ -0,0 +1,167 @@
#!/usr/bin/env python
import sys
import os
import re
from collections import OrderedDict
# scipy is kinda necessary
import scipy
import scipy.stats
import numpy as np
import math
def mean_nonan(l):
filtered = [x for x in l if not math.isnan(x)]
return np.mean(filtered)
def gmean_nonzero(l):
filtered = [x for x in l if x != 0 and not math.isnan(x)]
return scipy.stats.gmean(filtered)
libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../lib')
sys.path.append(libdir)
datadir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data')
import jdecode
import mtg_validate
import ngrams
def annotate_values(values):
for k in values:
(total, good, bad) = values[k]
values[k] = OrderedDict([('total', total), ('good', good), ('bad', bad)])
return values
def print_statistics(stats, ident = 0):
for k in stats:
if isinstance(stats[k], OrderedDict):
print(' ' * ident + str(k) + ':')
print_statistics(stats[k], ident=ident+2)
elif isinstance(stats[k], dict):
print(' ' * ident + str(k) + ': <dict with ' + str(len(stats[k])) + ' entries>')
elif isinstance(stats[k], list):
print(' ' * ident + str(k) + ': <list with ' + str(len(stats[k])) + ' entries>')
else:
print(' ' * ident + str(k) + ': ' + str(stats[k]))
def get_statistics(fname, lm = None, sep = False, verbose=False):
stats = OrderedDict()
cards = jdecode.mtg_open_file(fname, verbose=verbose)
stats['cards'] = cards
# unpack the name of the checkpoint - terrible and hacky
try:
final_name = os.path.basename(fname)
halves = final_name.split('_epoch')
cp_name = halves[0]
cp_info = halves[1][:-4]
info_halves = cp_info.split('_')
cp_epoch = float(info_halves[0])
fragments = info_halves[1].split('.')
cp_vloss = float('.'.join(fragments[:2]))
cp_temp = float('.'.join(fragments[-2:]))
cp_ident = '.'.join(fragments[2:-2])
stats['cp'] = OrderedDict([('name', cp_name),
('epoch', cp_epoch),
('vloss', cp_vloss),
('temp', cp_temp),
('ident', cp_ident)])
except Exception as e:
pass
# validate
((total_all, total_good, total_bad, total_uncovered),
values) = mtg_validate.process_props(cards)
stats['props'] = annotate_values(values)
stats['props']['overall'] = OrderedDict([('total', total_all),
('good', total_good),
('bad', total_bad),
('uncovered', total_uncovered)])
# distances
distfname = fname + '.dist'
if os.path.isfile(distfname):
name_dupes = 0
card_dupes = 0
with open(distfname, 'rt') as f:
distlines = f.read().split('\n')
dists = OrderedDict([('name', []), ('cbow', [])])
for line in distlines:
fields = line.split('|')
if len(fields) < 4:
continue
idx = int(fields[0])
name = str(fields[1])
ndist = float(fields[2])
cdist = float(fields[3])
dists['name'] += [ndist]
dists['cbow'] += [cdist]
if ndist == 1.0:
name_dupes += 1
if cdist == 1.0:
card_dupes += 1
dists['name_mean'] = mean_nonan(dists['name'])
dists['cbow_mean'] = mean_nonan(dists['cbow'])
dists['name_geomean'] = gmean_nonzero(dists['name'])
dists['cbow_geomean'] = gmean_nonzero(dists['cbow'])
stats['dists'] = dists
# n-grams
if not lm is None:
ngram = OrderedDict([('perp', []), ('perp_per', []),
('perp_max', []), ('perp_per_max', [])])
for card in cards:
if len(card.text.text) == 0:
perp = 0.0
perp_per = 0.0
elif sep:
vtexts = [line.vectorize().split() for line in card.text_lines
if len(line.vectorize().split()) > 0]
perps = [lm.perplexity(vtext) for vtext in vtexts]
perps_per = [perps[i] / float(len(vtexts[i])) for i in range(0, len(vtexts))]
perp = gmean_nonzero(perps)
perp_per = gmean_nonzero(perps_per)
perp_max = max(perps)
perp_per_max = max(perps_per)
else:
vtext = card.text.vectorize().split()
perp = lm.perplexity(vtext)
perp_per = perp / float(len(vtext))
perp_max = perp
perp_per_max = perps_per
ngram['perp'] += [perp]
ngram['perp_per'] += [perp_per]
ngram['perp_max'] += [perp_max]
ngram['perp_per_max'] += [perp_per_max]
ngram['perp_mean'] = mean_nonan(ngram['perp'])
ngram['perp_per_mean'] = mean_nonan(ngram['perp_per'])
ngram['perp_geomean'] = gmean_nonzero(ngram['perp'])
ngram['perp_per_geomean'] = gmean_nonzero(ngram['perp_per'])
stats['ngram'] = ngram
return stats
def main(infile, verbose = False):
lm = ngrams.build_ngram_model(jdecode.mtg_open_file(str(os.path.join(datadir, 'output.txt'))),
3, separate_lines=True, verbose=True)
stats = get_statistics(infile, lm=lm, sep=True, verbose=verbose)
print_statistics(stats)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('infile', #nargs='?'. default=None,
help='encoded card file or json corpus to process')
parser.add_argument('-v', '--verbose', action='store_true',
help='verbose output')
args = parser.parse_args()
main(args.infile, verbose=args.verbose)
exit(0)

91
scripts/autosample.py Executable file
View file

@ -0,0 +1,91 @@
#!/usr/bin/env python
import sys
import os
import subprocess
import random
def extract_cp_name(name):
# "lm_lstm_epoch50.00_0.1870.t7"
if not (name[:13] == 'lm_lstm_epoch' and name[-3:] == '.t7'):
return None
name = name[13:-3]
(epoch, vloss) = tuple(name.split('_'))
return (float(epoch), float(vloss))
def sample(cp, temp, count, seed = None, ident = 'output'):
if seed is None:
seed = random.randint(-1000000000, 1000000000)
outfile = cp + '.' + ident + '.' + str(temp) + '.txt'
cmd = ('th sample.lua ' + cp
+ ' -temperature ' + str(temp)
+ ' -length ' + str(count)
+ ' -seed ' + str(seed)
+ ' >> ' + outfile)
if os.path.exists(outfile):
print(outfile + ' already exists, skipping')
return False
else:
# UNSAFE SHELL=TRUE FOR CONVENIENCE
subprocess.call('echo "' + cmd + '" | tee ' + outfile, shell=True)
subprocess.call(cmd, shell=True)
def find_best_cp(cpdir):
best = None
best_cp = None
for path in os.listdir(cpdir):
fullpath = os.path.join(cpdir, path)
if os.path.isfile(fullpath):
extracted = extract_cp_name(path)
if not extracted is None:
(epoch, vloss) = extracted
if best is None or vloss < best:
best = vloss
best_cp = fullpath
return best_cp
def process_dir(cpdir, temp, count, seed = None, ident = 'output', verbose = False):
if verbose:
print('processing ' + cpdir)
best_cp = find_best_cp(cpdir)
if not best_cp is None:
sample(best_cp, temp, count, seed=seed, ident=ident)
for path in os.listdir(cpdir):
fullpath = os.path.join(cpdir, path)
if os.path.isdir(fullpath):
process_dir(fullpath, temp, count, seed=seed, ident=ident, verbose=verbose)
def main(rnndir, cpdir, temp, count, seed = None, ident = 'output', verbose = False):
if not os.path.isdir(rnndir):
raise ValueError('bad rnndir: ' + rnndir)
if not os.path.isdir(cpdir):
raise ValueError('bad cpdir: ' + cpdir)
os.chdir(rnndir)
process_dir(cpdir, temp, count, seed=seed, ident=ident, verbose=verbose)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('rnndir', #nargs='?'. default=None,
help='base rnn directory, must contain sample.lua')
parser.add_argument('cpdir', #nargs='?', default=None,
help='checkpoint directory, all subdirectories will be processed')
parser.add_argument('-t', '--temperature', action='store', default='1.0',
help='sampling temperature')
parser.add_argument('-c', '--count', action='store', default='1000000',
help='number of characters to sample each time')
parser.add_argument('-s', '--seed', action='store', default=None,
help='fixed seed; if not present, a random seed will be used')
parser.add_argument('-i', '--ident', action='store', default='output',
help='identifier to include in the output filenames')
parser.add_argument('-v', '--verbose', action='store_true',
help='verbose output')
args = parser.parse_args()
if args.seed is None:
seed = None
else:
seed = int(args.seed)
main(args.rnndir, args.cpdir, float(args.temperature), int(args.count),
seed=seed, ident=args.ident, verbose = args.verbose)
exit(0)

99
scripts/collect_checkpoints.py Executable file
View file

@ -0,0 +1,99 @@
#!/usr/bin/env python
import sys
import os
import shutil
def cleanup_dump(dumpstr):
cardfrags = dumpstr.split('\n\n')
if len(cardfrags) < 4:
return ''
else:
return '\n\n'.join(cardfrags[2:-1]) + '\n\n'
def identify_checkpoints(basedir, ident):
cp_infos = []
for path in os.listdir(basedir):
fullpath = os.path.join(basedir, path)
if not os.path.isfile(fullpath):
continue
if not (path[:13] == 'lm_lstm_epoch' and path[-4:] == '.txt'):
continue
if not ident in path:
continue
# attempt super hacky parsing
inner = path[13:-4]
halves = inner.split('_')
if not len(halves) == 2:
continue
parts = halves[1].split('.')
if not len(parts) == 6:
continue
# lm_lstm_epoch[25.00_0.3859.t7.output.1.0].txt
if not parts[3] == ident:
continue
epoch = halves[0]
vloss = '.'.join([parts[0], parts[1]])
temp = '.'.join([parts[4], parts[5]])
cpname = 'lm_lstm_epoch' + epoch + '_' + vloss + '.t7'
cp_infos += [(fullpath, os.path.join(basedir, cpname),
(epoch, vloss, temp))]
return cp_infos
def process_dir(basedir, targetdir, ident, copy_cp = False, verbose = False):
(basepath, basedirname) = os.path.split(basedir)
if basedirname == '':
(basepath, basedirname) = os.path.split(basepath)
cp_infos = identify_checkpoints(basedir, ident)
for (dpath, cpath, (epoch, vloss, temp)) in cp_infos:
if verbose:
print('found dumpfile ' + dpath)
dname = basedirname + '_epoch' + epoch + '_' + vloss + '.' + ident + '.' + temp + '.txt'
cname = basedirname + '_epoch' + epoch + '_' + vloss + '.t7'
tdpath = os.path.join(targetdir, dname)
tcpath = os.path.join(targetdir, cname)
if verbose:
print(' cpx ' + dpath + ' ' + tdpath)
with open(dpath, 'rt') as infile:
with open(tdpath, 'wt') as outfile:
outfile.write(cleanup_dump(infile.read()))
if copy_cp:
if os.path.isfile(cpath):
if verbose:
print(' cp ' + cpath + ' ' + tcpath)
shutil.copy(cpath, tcpath)
if copy_cp and len(cp_infos) > 0:
cmdpath = os.path.join(basedir, 'command.txt')
tcmdpath = os.path.join(targetdir, basedirname + '.command')
if os.path.isfile(cmdpath):
if verbose:
print(' cp ' + cmdpath + ' ' + tcmdpath)
shutil.copy(cmdpath, tcmdpath)
for path in os.listdir(basedir):
fullpath = os.path.join(basedir, path)
if os.path.isdir(fullpath):
process_dir(fullpath, targetdir, ident, copy_cp=copy_cp, verbose=verbose)
def main(basedir, targetdir, ident = 'output', copy_cp = False, verbose = False):
process_dir(basedir, targetdir, ident, copy_cp=copy_cp, verbose=verbose)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('basedir', #nargs='?'. default=None,
help='base rnn directory, must contain sample.lua')
parser.add_argument('targetdir', #nargs='?', default=None,
help='checkpoint directory, all subdirectories will be processed')
parser.add_argument('-c', '--copy_cp', action='store_true',
help='copy checkpoints used to generate the output files')
parser.add_argument('-i', '--ident', action='store', default='output',
help='identifier to look for to determine checkpoints')
parser.add_argument('-v', '--verbose', action='store_true',
help='verbose output')
args = parser.parse_args()
main(args.basedir, args.targetdir, ident=args.ident, copy_cp=args.copy_cp, verbose=args.verbose)
exit(0)

81
scripts/distances.py Executable file
View file

@ -0,0 +1,81 @@
#!/usr/bin/env python
import sys
import os
libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../lib')
sys.path.append(libdir)
import utils
import jdecode
from namediff import Namediff
from cbow import CBOW
def main(fname, oname, verbose = True, parallel = True):
# may need to set special arguments here
cards = jdecode.mtg_open_file(fname, verbose=verbose)
# this could reasonably be some separate function
# might make sense to merge cbow and namediff and have this be the main interface
namediff = Namediff()
cbow = CBOW()
if verbose:
print 'Computing nearest names...'
if parallel:
nearest_names = namediff.nearest_par(map(lambda c: c.name, cards), n=1)
else:
nearest_names = [namediff.nearest(c.name, n=1) for c in cards]
if verbose:
print 'Computing nearest cards...'
if parallel:
nearest_cards = cbow.nearest_par(cards, n=1)
else:
nearest_cards = [cbow.nearest(c, n=1) for c in cards]
for i in range(0, len(cards)):
cards[i].nearest_names = nearest_names[i]
cards[i].nearest_cards = nearest_cards[i]
# # unfortunately this takes ~30 hours on 8 cores for a 10MB dump
# if verbose:
# print 'Computing nearest encodings by text edit distance...'
# if parallel:
# nearest_cards_text = namediff.nearest_card_par(cards, n=1)
# else:
# nearest_cards_text = [namediff.nearest_card(c, n=1) for c in cards]
if verbose:
print '...Done.'
# write to a file to store the data, this is a terribly long computation
# we could also just store this same info in the cards themselves as more fields...
sep = '|'
with open(oname, 'w') as ofile:
for i in range(0, len(cards)):
card = cards[i]
ostr = str(i) + sep + card.name + sep
ndist, _ = card.nearest_names[0]
ostr += str(ndist) + sep
cdist, _ = card.nearest_cards[0]
ostr += str(cdist) + '\n'
# tdist, _ = nearest_cards_text[i][0]
# ostr += str(tdist) + '\n'
ofile.write(ostr.encode('utf-8'))
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('infile', #nargs='?'. default=None,
help='encoded card file or json corpus to process')
parser.add_argument('outfile', #nargs='?', default=None,
help='name of output file, will be overwritten')
parser.add_argument('-v', '--verbose', action='store_true',
help='verbose output')
parser.add_argument('-p', '--parallel', action='store_true',
help='run in parallel on all cores')
args = parser.parse_args()
main(args.infile, args.outfile, verbose=args.verbose, parallel=args.parallel)
exit(0)

81
scripts/keydiff.py Executable file
View file

@ -0,0 +1,81 @@
#!/usr/bin/env python
def parse_keyfile(f, d, constructor = lambda x: x):
for line in f:
kv = map(lambda s: s.strip(), line.split(':'))
if not len(kv) == 2:
continue
d[kv[0]] = constructor(kv[1])
def merge_dicts(d1, d2):
d = {}
for k in d1:
d[k] = (d1[k], d2[k] if k in d2 else None)
for k in d2:
if not k in d:
d[k] = (None, d2[k])
return d
def main(fname1, fname2, verbose = True):
if verbose:
print 'opening ' + fname1 + ' as base key/value store'
print 'opening ' + fname2 + ' as target key/value store'
d1 = {}
d2 = {}
with open(fname1, 'rt') as f1:
parse_keyfile(f1, d1, int)
with open(fname2, 'rt') as f2:
parse_keyfile(f2, d2, int)
tot1 = sum(d1.values())
tot2 = sum(d2.values())
if verbose:
print ' ' + fname1 + ': ' + str(len(d1)) + ', total ' + str(tot1)
print ' ' + fname2 + ': ' + str(len(d2)) + ', total ' + str(tot2)
d_merged = merge_dicts(d1, d2)
ratios = {}
only_1 = {}
only_2 = {}
for k in d_merged:
(v1, v2) = d_merged[k]
if v1 is None:
only_2[k] = v2
elif v2 is None:
only_1[k] = v1
else:
ratios[k] = float(v2 * tot1) / float(v1 * tot2)
print 'shared: ' + str(len(ratios))
for k in sorted(ratios, lambda x,y: cmp(d2[x], d2[y]), reverse=True):
print ' ' + k + ': ' + str(d2[k]) + '/' + str(d1[k]) + ' (' + str(ratios[k]) + ')'
print ''
print '1 only: ' + str(len(only_1))
for k in sorted(only_1, lambda x,y: cmp(d1[x], d1[y]), reverse=True):
print ' ' + k + ': ' + str(d1[k])
print ''
print '2 only: ' + str(len(only_2))
for k in sorted(only_2, lambda x,y: cmp(d2[x], d2[y]), reverse=True):
print ' ' + k + ': ' + str(d2[k])
print ''
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('file1', #nargs='?'. default=None,
help='base key file to diff against')
parser.add_argument('file2', nargs='?', default=None,
help='other file to compare against the baseline')
parser.add_argument('-v', '--verbose', action='store_true',
help='verbose output')
args = parser.parse_args()
main(args.file1, args.file2, verbose=args.verbose)
exit(0)

482
scripts/mtg_validate.py Executable file
View file

@ -0,0 +1,482 @@
#!/usr/bin/env python
import sys
import os
import re
from collections import OrderedDict
libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../lib')
sys.path.append(libdir)
import utils
import jdecode
datadir = os.path.realpath(os.path.join(libdir, '../data'))
gramdir = os.path.join(datadir, 'ngrams')
compute_ngrams = False
gramdicts = {}
if os.path.isdir(gramdir):
import keydiff
compute_ngrams = True
for fname in os.listdir(gramdir):
suffixes = re.findall(r'\.[0-9]*g$', fname)
if suffixes:
grams = int(suffixes[0][1:-1])
d = {}
with open(os.path.join(gramdir, fname), 'rt') as f:
keydiff.parse_keyfile(f, d, int)
gramdicts[grams] = d
def rare_grams(card, thresh = 2, grams = 2):
if not grams in gramdicts:
return None
rares = 0
gramdict = gramdicts[grams]
for line in card.text_lines_words:
for i in range(0, len(line) - (grams - 1)):
ngram = ' '.join([line[i + j] for j in range(0, grams)])
if ngram in gramdict:
if gramdict[ngram] < thresh:
rares += 1
else:
rares += 1
return rares
def list_only(l, items):
for e in l:
if not e in items:
return False
return True
def pct(x, total):
pctstr = 100.0 * float(x) / float(total)
return '(' + str(pctstr)[:5] + '%)'
def check_types(card):
if 'instant' in card.types:
return list_only(card.types, ['tribal', 'instant'])
if 'sorcery' in card.types:
return list_only(card.types, ['tribal', 'sorcery'])
if 'creature' in card.types:
return list_only(card.types, ['tribal', 'creature', 'artifact', 'land', 'enchantment'])
if 'planeswalker' in card.types:
return list_only(card.types, ['tribal', 'planeswalker', 'artifact', 'land', 'enchantment'])
else:
return list_only(card.types, ['tribal', 'artifact', 'land', 'enchantment'])
def check_pt(card):
if 'creature' in card.types or card.pt:
return (('creature' in card.types and len(re.findall(re.escape('/'), card.pt)) == 1)
and not card.loyalty)
if 'planeswalker' in card.types or card.loyalty:
return (('planeswalker' in card.types and card.loyalty)
and not card.pt)
return None
def check_lands(card):
if 'land' in card.types:
return card.cost.format() == '_NOCOST_'
else:
return None
# doesn't handle granted activated abilities in ""
def check_X(card):
correct = None
incost = 'X' in card.cost.encode()
extra_cost_lines = 0
cost_lines = 0
use_lines = 0
for mt in card.text_lines:
sides = mt.text.split(':')
if len(sides) == 2:
actcosts = len(re.findall(re.escape(utils.reserved_mana_marker), sides[0]))
lcosts = mt.costs[:actcosts]
rcosts = mt.costs[actcosts:]
if 'X' in sides[0] or (utils.reserved_mana_marker in sides[0] and
'X' in ''.join(map(lambda c: c.encode(), lcosts))):
if incost:
return False # bad, duplicated Xs in costs
if 'X' in sides[1] or (utils.reserved_mana_marker in sides[1] and
'X' in ''.join(map(lambda c: c.encode(), rcosts))):
correct = True # good, defined X is either specified or used
if 'monstrosity' in sides[1]:
extra_cost_lines += 1
continue
elif 'remove X % counters' in sides[0] and 'each counter removed' in sides[1]:
correct = True # Blademane Baku
continue
elif 'note' in sides[1]:
correct = True # Ice Cauldron
continue
else:
return False # bad, defined X is unused
# we've checked all cases where an X ocurrs in an activiation cost
linetext = mt.encode()
intext = len(re.findall(r'X', linetext))
defs = (len(re.findall(r'X is', linetext))
+ len(re.findall(re.escape('pay {X'), linetext))
+ len(re.findall(re.escape('pay X'), linetext))
+ len(re.findall(re.escape('reveal X'), linetext))
+ len(re.findall(re.escape('may tap X'), linetext)))
if incost:
if intext:
correct = True # defined and used or specified in some way
elif intext > 0:
if intext > 1 and defs > 0:
correct = True # look for multiples
elif 'suspend' in linetext or 'bloodthirst' in linetext:
correct = True # special case keywords
elif 'reinforce' in linetext and intext > 2:
correct = True # this should work
elif 'contain {X' in linetext or 'with {X' in linetext:
correct = True
elif ('additional cost' in linetext
or 'morph' in linetext
or 'kicker' in linetext):
cost_lines += 1
else:
use_lines += 1
if incost and not correct:
if 'sunburst' in card.text.text or 'spent to cast' in card.text.text:
return True # Engineered Explosives, Skyrider Elf
return False # otherwise we should have seen X somewhere if it was in the cost
elif cost_lines > 0 or use_lines > 0:
if (cost_lines + extra_cost_lines) == 1 and use_lines > 0:
return True # dreams, etc.
else:
return False
return correct
def check_kicker(card):
# also lazy and simple
if 'kicker' in card.text.text or 'kicked' in card.text.text:
# could also check for costs, at least make 'it's $ kicker,' not count as a kicker ability
newtext = card.text.text.replace(utils.reserved_mana_marker + ' kicker', '')
return 'kicker' in newtext and 'kicked' in newtext
else:
return None
def check_counters(card):
uses = len(re.findall(re.escape(utils.counter_marker), card.text.text))
if uses > 0:
return uses > 1 and 'countertype ' + utils.counter_marker in card.text.text
else:
return None
def check_choices(card):
bullets = len(re.findall(re.escape(utils.bullet_marker), card.text.text))
obracks = len(re.findall(re.escape(utils.choice_open_delimiter), card.text.text))
cbracks = len(re.findall(re.escape(utils.choice_close_delimiter), card.text.text))
if bullets + obracks + cbracks > 0:
if not (obracks == cbracks and bullets > 0):
return False
# could compile ahead of time
choice_regex = (re.escape(utils.choice_open_delimiter) + re.escape(utils.unary_marker)
+ r'.*' + re.escape(utils.bullet_marker) + r'.*'
+ re.escape(utils.choice_close_delimiter))
nochoices = re.sub(choice_regex, '', card.text.text)
nobullets = len(re.findall(re.escape(utils.bullet_marker), nochoices))
noobracks = len(re.findall(re.escape(utils.choice_open_delimiter), nochoices))
nocbracks = len(re.findall(re.escape(utils.choice_close_delimiter), nochoices))
return nobullets + noobracks + nocbracks == 0
else:
return None
def check_auras(card):
# a bit loose
if 'enchantment' in card.types or 'aura' in card.subtypes or 'enchant' in card.text.text:
return 'enchantment' in card.types or 'aura' in card.subtypes or 'enchant' in card.text.text
else:
return None
def check_equipment(card):
# probably even looser, chould check for actual equip abilities and noncreatureness
if 'equipment' in card.subtypes:
return 'equip' in card.text.text
else:
return None
def check_planeswalkers(card):
if 'planeswalker' in card.types:
good_lines = 0
bad_lines = 0
initial_re = r'^[+-]?' + re.escape(utils.unary_marker) + re.escape(utils.unary_counter) + '*:'
initial_re_X = r'^[-+]' + re.escape(utils.x_marker) + '+:'
for line in card.text_lines:
if len(re.findall(initial_re, line.text)) == 1:
good_lines += 1
elif len(re.findall(initial_re_X, line.text)) == 1:
good_lines += 1
elif 'can be your commander' in line.text:
pass
elif 'countertype' in line.text or 'transform' in line.text:
pass
else:
bad_lines += 1
return good_lines > 1 and bad_lines == 0
else:
return None
def check_levelup(card):
if 'level' in card.text.text:
uplines = 0
llines = 0
for line in card.text_lines:
if 'countertype ' + utils.counter_marker + ' level' in line.text:
uplines += 1
llines += 1
elif 'with level up' in line.text:
llines += 1
elif 'level up' in line.text:
uplines += 1
elif 'level' in line.text:
llines += 1
return uplines == 1 and llines > 0
else:
return None
def check_activated(card):
activated = 0
for line in card.text_lines:
if '.' in line.text:
subtext = re.sub(r'"[^"]*"', '', line.text)
if 'forecast' in subtext:
pass
elif 'return ' + utils.this_marker + ' from your graveyard' in subtext:
pass
elif 'on the stack' in subtext:
pass
elif ':' in subtext:
activated += 1
if activated > 0:
return list_only(card.types, ['creature', 'land', 'artifact', 'enchantment', 'planeswalker', 'tribal'])
else:
return None
def check_triggered(card):
triggered = 0
triggered_2 = 0
for line in card.text_lines:
if 'when ' + utils.this_marker + ' enters the battlefield' in line.text:
triggered += 1
if 'when ' + utils.this_marker + ' leaves the battlefield' in line.text:
triggered += 1
if 'when ' + utils.this_marker + ' dies' in line.text:
triggered += 1
elif 'at the beginning' == line.text[:16] or 'when' == line.text[:4]:
if 'from your graveyard' in line.text:
triggered_2 += 1
elif 'in your graveyard' in line.text:
triggered_2 += 1
elif 'if ' + utils.this_marker + ' is suspended' in line.text:
triggered_2 += 1
elif 'if that card is exiled' in line.text or 'if ' + utils.this_marker + ' is exiled' in line.text:
triggered_2 += 1
elif 'when the creature ' + utils.this_marker + ' haunts' in line.text:
triggered_2 += 1
elif 'when you cycle ' + utils.this_marker in line.text or 'when you cast ' + utils.this_marker in line.text:
triggered_2 += 1
elif 'this turn' in line.text or 'this combat' in line.text or 'your next upkeep' in line.text:
triggered_2 += 1
elif 'from your library' in line.text:
triggered_2 += 1
elif 'you discard ' + utils.this_marker in line.text or 'you to discard ' + utils.this_marker in line.text:
triggered_2 += 1
else:
triggered += 1
if triggered > 0:
return list_only(card.types, ['creature', 'land', 'artifact', 'enchantment', 'planeswalker', 'tribal'])
elif triggered_2:
return True
else:
return None
def check_chosen(card):
if 'chosen' in card.text.text:
return ('choose' in card.text.text
or 'chosen at random' in card.text.text
or 'name' in card.text.text
or 'is chosen' in card.text.text
or 'search' in card.text.text)
else:
return None
def check_shuffle(card):
retval = None
# sadly, this does not detect spurious shuffling
for line in card.text_lines:
if 'search' in line.text and 'library' in line.text:
thisval = ('shuffle' in line.text
or 'searches' in line.text
or 'searched' in line.text
or 'searching' in line.text
or 'rest' in line.text
or 'instead' in line.text)
if retval is None:
retval = thisval
else:
retval = retval and thisval
return retval
def check_quotes(card):
retval = None
for line in card.text_lines:
quotes = len(re.findall(re.escape('"'), line.text))
# HACK: the '" pattern in the training set is actually incorrect
quotes += len(re.findall(re.escape('\'"'), line.text))
if quotes > 0:
thisval = quotes % 2 == 0
if retval is None:
retval = thisval
else:
retval = retval and thisval
return retval
props = OrderedDict([
('types', check_types),
('pt', check_pt),
('lands', check_lands),
('X', check_X),
('kicker', check_kicker),
('counters', check_counters),
('choices', check_choices),
('quotes', check_quotes),
('auras', check_auras),
('equipment', check_equipment),
('planeswalkers', check_planeswalkers),
('levelup', check_levelup),
('chosen', check_chosen),
('shuffle', check_shuffle),
('activated', check_activated),
('triggered', check_triggered),
])
def process_props(cards, dump = False, uncovered = False):
total_all = 0
total_good = 0
total_bad = 0
total_uncovered = 0
values = OrderedDict([(k, (0,0,0)) for k in props])
for card in cards:
total_all += 1
overall = True
any_prop = False
for prop in props:
(total, good, bad) = values[prop]
this_prop = props[prop](card)
if not this_prop is None:
total += 1
if not prop == 'types':
any_prop = True
if this_prop:
good += 1
else:
bad += 1
overall = False
if card.name not in ['demonic pact', 'lavaclaw reaches',
"ertai's trickery", 'rumbling aftershocks', # i hate these
] and dump:
print('---- ' + prop + ' ----')
print(card.encode())
print(card.format())
values[prop] = (total, good, bad)
if overall:
total_good += 1
else:
total_bad += 1
if not any_prop:
total_uncovered += 1
if uncovered:
print('---- uncovered ----')
print(card.encode())
print(card.format())
return ((total_all, total_good, total_bad, total_uncovered),
values)
def main(fname, oname = None, verbose = False, dump = False):
# may need to set special arguments here
cards = jdecode.mtg_open_file(fname, verbose=verbose)
do_grams = False
if do_grams:
rg = {}
for card in cards:
g = rare_grams(card, thresh=2, grams=2)
if len(card.text_words) > 0:
g = int(1.0 + (float(g) * 100.0 / float(len(card.text_words))))
if g in rg:
rg[g] += 1
else:
rg[g] = 1
if g >= 60:
print g
print card.format()
tot = 0
vmax = sum(rg.values())
pct90 = None
pct95 = None
pct99 = None
for i in sorted(rg):
print str(i) + ' rare ngrams: ' + str(rg[i])
tot += rg[i]
if pct90 is None and tot >= vmax * 0.90:
pct90 = i
if pct95 is None and tot >= vmax * 0.95:
pct95 = i
if pct99 is None and tot >= vmax * 0.99:
pct99 = i
print '90% - ' + str(pct90)
print '95% - ' + str(pct95)
print '99% - ' + str(pct99)
else:
((total_all, total_good, total_bad, total_uncovered),
values) = process_props(cards, dump=dump)
# summary
print('-- overall --')
print(' total : ' + str(total_all))
print(' good : ' + str(total_good) + ' ' + pct(total_good, total_all))
print(' bad : ' + str(total_bad) + ' ' + pct(total_bad, total_all))
print(' uncocoverd: ' + str(total_uncovered) + ' ' + pct(total_uncovered, total_all))
print('----')
# breakdown
for prop in props:
(total, good, bad) = values[prop]
print(prop + ':')
print(' total: ' + str(total) + ' ' + pct(total, total_all))
print(' good : ' + str(good) + ' ' + pct(good, total_all))
print(' bad : ' + str(bad) + ' ' + pct(bad, total_all))
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('infile', #nargs='?'. default=None,
help='encoded card file or json corpus to process')
parser.add_argument('outfile', nargs='?', default=None,
help='name of output file, will be overwritten')
parser.add_argument('-v', '--verbose', action='store_true',
help='verbose output')
parser.add_argument('-d', '--dump', action='store_true',
help='print invalid cards')
args = parser.parse_args()
main(args.infile, args.outfile, verbose=args.verbose, dump=args.dump)
exit(0)

123
scripts/ngrams.py Executable file
View file

@ -0,0 +1,123 @@
#!/usr/bin/env python
import sys
import os
import pickle
libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../lib')
sys.path.append(libdir)
import jdecode
import nltk_model as model
def update_ngrams(lines, gramdict, grams):
for line in lines:
for i in range(0, len(line) - (grams - 1)):
ngram = ' '.join([line[i + j] for j in range(0, grams)])
if ngram in gramdict:
gramdict[ngram] += 1
else:
gramdict[ngram] = 1
def describe_bins(gramdict, bins):
bins = sorted(bins)
counts = [0 for _ in range(0, len(bins) + 1)]
for ngram in gramdict:
for i in range(0, len(bins) + 1):
if i < len(bins):
if gramdict[ngram] <= bins[i]:
counts[i] += 1
break
else:
# didn't fit into any of the smaller bins, stick in on the end
counts[-1] += 1
for i in range(0, len(counts)):
if counts[i] > 0:
print (' ' + (str(bins[i]) if i < len(bins) else str(bins[-1]) + '+')
+ ': ' + str(counts[i]))
def extract_language(cards, separate_lines = True):
if separate_lines:
lang = [line.vectorize() for card in cards for line in card.text_lines]
else:
lang = [card.text.vectorize() for card in cards]
return map(lambda s: s.split(), lang)
def build_ngram_model(cards, n, separate_lines = True, verbose = False):
if verbose:
print('generating ' + str(n) + '-gram model')
lang = extract_language(cards, separate_lines=separate_lines)
if verbose:
print('found ' + str(len(lang)) + ' sentences')
lm = model.NgramModel(n, lang, pad_left=True, pad_right=True)
if verbose:
print(lm)
return lm
def main(fname, oname, gmin = 2, gmax = 8, nltk = False, sep = False, verbose = False):
# may need to set special arguments here
cards = jdecode.mtg_open_file(fname, verbose=verbose)
gmin = int(gmin)
gmax = int(gmax)
if nltk:
n = gmin
lm = build_ngram_model(cards, n, separate_lines=sep, verbose=verbose)
if verbose:
teststr = 'when @ enters the battlefield'
print('litmus test: perplexity of ' + repr(teststr))
print(' ' + str(lm.perplexity(teststr.split())))
if verbose:
print('pickling module to ' + oname)
with open(oname, 'wb') as f:
pickle.dump(lm, f)
else:
bins = [1, 2, 3, 10, 30, 100, 300, 1000]
if gmin < 2 or gmax < gmin:
print 'invalid gram sizes: ' + str(gmin) + '-' + str(gmax)
exit(1)
for grams in range(gmin, gmax+1):
if verbose:
print 'generating ' + str(grams) + '-grams...'
gramdict = {}
for card in cards:
update_ngrams(card.text_lines_words, gramdict, grams)
oname_full = oname + '.' + str(grams) + 'g'
if verbose:
print(' writing ' + str(len(gramdict)) + ' unique ' + str(grams)
+ '-grams to ' + oname_full)
describe_bins(gramdict, bins)
with open(oname_full, 'wt') as f:
for ngram in sorted(gramdict,
lambda x,y: cmp(gramdict[x], gramdict[y]),
reverse = True):
f.write((ngram + ': ' + str(gramdict[ngram]) + '\n').encode('utf-8'))
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('infile', #nargs='?'. default=None,
help='encoded card file or json corpus to process')
parser.add_argument('outfile', #nargs='?', default=None,
help='base name of output file, outputs ending in .2g, .3g etc. will be produced')
parser.add_argument('-min', '--min', action='store', default='2',
help='minimum gram size to compute')
parser.add_argument('-max', '--max', action='store', default='8',
help='maximum gram size to compute')
parser.add_argument('-nltk', '--nltk', action='store_true',
help='use nltk model.NgramModel, with n = min')
parser.add_argument('-s', '--separate', action='store_true',
help='separate card text into lines when constructing nltk model')
parser.add_argument('-v', '--verbose', action='store_true',
help='verbose output')
args = parser.parse_args()
main(args.infile, args.outfile, gmin=args.min, gmax=args.max, nltk=args.nltk,
sep=args.separate, verbose=args.verbose)
exit(0)

159
scripts/pairing.py Executable file
View file

@ -0,0 +1,159 @@
#!/usr/bin/env python
import sys
import os
import random
import zipfile
import shutil
libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../lib')
sys.path.append(libdir)
datadir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data')
import utils
import jdecode
import ngrams
import analysis
import mtg_validate
from cbow import CBOW
separate_lines=True
def select_card(cards, stats, i):
card = cards[i]
nearest = stats['dists']['cbow'][i]
perp = stats['ngram']['perp'][i]
perp_per = stats['ngram']['perp_per'][i]
perp_max = stats['ngram']['perp_max'][i]
if nearest > 0.9 or perp_per > 2.0 or perp_max > 10.0:
return None
((_, total_good, _, _), _) = mtg_validate.process_props([card])
if not total_good == 1:
return False
# print '===='
# print nearest
# print perp
# print perp_per
# print perp_max
# print '----'
# print card.format()
return True
def compare_to_real(card, realcard):
ctypes = ' '.join(sorted(card.types))
rtypes = ' '.join(sorted(realcard.types))
return ctypes == rtypes and realcard.cost.check_colors(card.cost.get_colors())
def writecard(card, name, writer):
gatherer = False
for_forum = True
vdump = True
fmt = card.format(gatherer = gatherer, for_forum = for_forum, vdump = vdump)
oldname = card.name
# alter name used in image
card.name = name
writer.write(card.to_mse().encode('utf-8'))
card.name = oldname
fstring = ''
if card.json:
fstring += 'JSON:\n' + card.json + '\n'
if card.raw:
fstring += 'raw:\n' + card.raw + '\n'
fstring += '\n'
fstring += fmt + '\n'
fstring = fstring.replace('<', '(').replace('>', ')')
writer.write(('\n' + fstring[:-1]).replace('\n', '\n\t\t').encode('utf-8'))
writer.write('\n'.encode('utf-8'))
def main(fname, oname, n=20, verbose=False):
cbow = CBOW()
realcards = jdecode.mtg_open_file(str(os.path.join(datadir, 'output.txt')), verbose=verbose)
real_by_name = {c.name: c for c in realcards}
lm = ngrams.build_ngram_model(realcards, 3, separate_lines=separate_lines, verbose=verbose)
cards = jdecode.mtg_open_file(fname, verbose=verbose)
stats = analysis.get_statistics(fname, lm=lm, sep=separate_lines, verbose=verbose)
selected = []
for i in range(0, len(cards)):
if select_card(cards, stats, i):
selected += [(i, cards[i])]
limit = 3000
random.shuffle(selected)
#selected = selected[:limit]
if verbose:
print('computing nearest cards for ' + str(len(selected)) + ' candindates...')
cbow_nearest = cbow.nearest_par(map(lambda (i, c): c, selected))
for i in range(0, len(selected)):
(j, card) = selected[i]
selected[i] = (j, card, cbow_nearest[i])
if verbose:
print('...done')
final = []
for (i, card, nearest) in selected:
for dist, rname in nearest:
realcard = real_by_name[rname]
if compare_to_real(card, realcard):
final += [(i, card, realcard, dist)]
break
for (i, card, realcard, dist) in final:
print '-- real --'
print realcard.format()
print '-- fake --'
print card.format()
print '-- stats --'
perp_per = stats['ngram']['perp_per'][i]
perp_max = stats['ngram']['perp_max'][i]
print dist
print perp_per
print perp_max
print '----'
if not oname is None:
with open(oname, 'wt') as ofile:
ofile.write(utils.mse_prepend)
for (i, card, realcard, dist) in final:
name = realcard.name
writecard(realcard, name, ofile)
writecard(card, name, ofile)
ofile.write('version control:\n\ttype: none\napprentice code: ')
# Copy whatever output file is produced, name the copy 'set' (yes, no extension).
if os.path.isfile('set'):
print 'ERROR: tried to overwrite existing file "set" - aborting.'
return
shutil.copyfile(oname, 'set')
# Use the freaky mse extension instead of zip.
with zipfile.ZipFile(oname+'.mse-set', mode='w') as zf:
try:
# Zip up the set file into oname.mse-set.
zf.write('set')
finally:
if verbose:
print 'Made an MSE set file called ' + oname + '.mse-set.'
# The set file is useless outside the .mse-set, delete it.
os.remove('set')
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('infile', #nargs='?'. default=None,
help='encoded card file or json corpus to process')
parser.add_argument('outfile', nargs='?', default=None,
help='output file, defaults to none')
parser.add_argument('-n', '--n', action='store',
help='number of cards to consider for each pairing')
parser.add_argument('-v', '--verbose', action='store_true',
help='verbose output')
args = parser.parse_args()
main(args.infile, args.outfile, n=args.n, verbose=args.verbose)
exit(0)

172
scripts/sanity.py Executable file
View file

@ -0,0 +1,172 @@
#!/usr/bin/env python
import sys
import os
import re
import json
libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../lib')
sys.path.append(libdir)
import utils
import jdecode
import cardlib
import transforms
def check_lines(fname):
cards = jdecode.mtg_open_file(fname, verbose=True, linetrans=True)
prelines = set()
keylines = set()
mainlines = set()
costlines = set()
postlines = set()
known = ['enchant ', 'equip', 'countertype', 'multikicker', 'kicker',
'suspend', 'echo', 'awaken', 'bestow', 'buyback',
'cumulative', 'dash', 'entwine', 'evoke', 'fortify',
'flashback', 'madness', 'morph', 'megamorph', 'miracle', 'ninjutsu',
'overload', 'prowl', 'recover', 'reinforce', 'replicate', 'scavenge',
'splice', 'surge', 'unearth', 'transfigure', 'transmute',
]
known = []
for card in cards:
prel, keyl, mainl, costl, postl = transforms.separate_lines(card.text.encode(randomize=False))
if card.bside:
prel2, keyl2, mainl2, costl2, postl2 = transforms.separate_lines(card.bside.text.encode(randomize=False))
prel += prel2
keyl += keyl2
mainl += mainl2
costl += costl2
postl += postl2
for line in prel:
if line.strip() == '':
print(card.name, card.text.text)
if any(line.startswith(s) for s in known):
line = 'known'
prelines.add(line)
for line in postl:
if line.strip() == '':
print(card.name, card.text.text)
if any(line.startswith(s) for s in known):
line = 'known'
postlines.add(line)
for line in keyl:
if line.strip() == '':
print(card.name, card.text.text)
if any(line.startswith(s) for s in known):
line = 'known'
keylines.add(line)
for line in mainl:
if line.strip() == '':
print(card.name, card.text.text)
# if any(line.startswith(s) for s in known):
# line = 'known'
mainlines.add(line)
for line in costl:
if line.strip() == '':
print(card.name, card.text.text)
# if any(line.startswith(s) for s in known) or 'cycling' in line or 'monstrosity' in line:
# line = 'known'
costlines.add(line)
print('prel: {:d}, keyl: {:d}, mainl: {:d}, postl {:d}'
.format(len(prelines), len(keylines), len(mainlines), len(postlines)))
print('\nprelines')
for line in sorted(prelines):
print(line)
print('\npostlines')
for line in sorted(postlines):
print(line)
print('\ncostlines')
for line in sorted(costlines):
print(line)
print('\nkeylines')
for line in sorted(keylines):
print(line)
print('\nmainlines')
for line in sorted(mainlines):
#if any(s in line for s in ['champion', 'devour', 'tribute']):
print(line)
def check_vocab(fname):
cards = jdecode.mtg_open_file(fname, verbose=True, linetrans=True)
vocab = {}
for card in cards:
words = card.text.vectorize().split()
if card.bside:
words += card.bside.text.vectorize().split()
for word in words:
if not word in vocab:
vocab[word] = 1
else:
vocab[word] += 1
for word in sorted(vocab, lambda x,y: cmp(vocab[x], vocab[y]), reverse = True):
print('{:8d} : {:s}'.format(vocab[word], word))
n = 3
for card in cards:
words = card.text.vectorize().split()
if card.bside:
words += card.bside.text.vectorize().split()
for word in words:
if vocab[word] <= n:
#if 'name' in word:
print('\n{:8d} : {:s}'.format(vocab[word], word))
print(card.encode())
break
def check_characters(fname, vname):
cards = jdecode.mtg_open_file(fname, verbose=True, linetrans=True)
tokens = {c for c in utils.cardsep}
for card in cards:
for c in card.encode():
tokens.add(c)
token_to_idx = {tok:i+1 for i, tok in enumerate(sorted(tokens))}
idx_to_token = {i+1:tok for i, tok in enumerate(sorted(tokens))}
print('Vocabulary: ({:d} symbols)'.format(len(token_to_idx)))
for token in sorted(token_to_idx):
print('{:8s} : {:4d}'.format(repr(token), token_to_idx[token]))
# compliant with torch-rnn
if vname:
json_data = {'token_to_idx':token_to_idx, 'idx_to_token':idx_to_token}
print('writing vocabulary to {:s}'.format(vname))
with open(vname, 'w') as f:
json.dump(json_data, f)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('infile', nargs='?', default=os.path.join(libdir, '../data/output.txt'),
help='encoded card file or json corpus to process')
parser.add_argument('-lines', action='store_true',
help='show behavior of line separation')
parser.add_argument('-vocab', action='store_true',
help='show vocabulary counts from encoded card text')
parser.add_argument('-chars', action='store_true',
help='generate and display vocabulary of characters used in encoding')
parser.add_argument('--vocab_name', default=None,
help='json file to write vocabulary to')
args = parser.parse_args()
if args.lines:
check_lines(args.infile)
if args.vocab:
check_vocab(args.infile)
if args.chars:
check_characters(args.infile, args.vocab_name)
exit(0)

122
scripts/streamcards.py Executable file
View file

@ -0,0 +1,122 @@
#!/usr/bin/env python
# -- STOLEN FROM torch-rnn/scripts/streamfile.py -- #
import os
import threading
import time
import signal
import traceback
import psutil
# correctly setting up a stream that won't get orphaned and left clutting the operating
# system proceeds in 3 parts:
# 1) invoke install_suicide_handlers() to ensure correct behavior on interrupt
# 2) get threads by invoking spawn_stream_threads
# 3) invoke wait_and_kill_self_noreturn(threads)
# or, use the handy wrapper that does it for you
def spawn_stream_threads(fds, runthread, mkargs):
threads = []
for i, fd in enumerate(fds):
stream_thread = threading.Thread(target=runthread, args=mkargs(i, fd))
stream_thread.daemon = True
stream_thread.start()
threads.append(stream_thread)
return threads
def force_kill_self_noreturn():
# We have a strange issue here, which is that our threads will refuse to die
# to a normal exit() or sys.exit() because they're all blocked in write() calls
# on full pipes; the simplest workaround seems to be to ask the OS to terminate us.
# This kinda works, but...
#os.kill(os.getpid(), signal.SIGTERM)
# psutil might have useful features like checking if the pid has been reused before killing it.
# Also we might have child processes like l2e luajits to think about.
me = psutil.Process(os.getpid())
for child in me.children(recursive=True):
child.terminate()
me.terminate()
def handler_kill_self(signum, frame):
if signum != signal.SIGQUIT:
traceback.print_stack(frame)
print('caught signal {:d} - streamer sending SIGTERM to self'.format(signum))
force_kill_self_noreturn()
def install_suicide_handlers():
for sig in [signal.SIGHUP, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, handler_kill_self)
def wait_and_kill_self_noreturn(threads):
running = True
while running:
running = False
for thread in threads:
if thread.is_alive():
running = True
if(os.getppid() <= 1):
# exit if parent process died (and we were reparented to init)
break
time.sleep(1)
force_kill_self_noreturn()
def streaming_noreturn(fds, write_stream, mkargs):
install_suicide_handlers()
threads = spawn_stream_threads(fds, write_stream, mkargs)
wait_and_kill_self_noreturn(threads)
assert False, 'should not return from streaming'
# -- END STOLEN FROM torch-rnn/scripts/streamfile.py -- #
import sys
import random
libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../lib')
sys.path.append(libdir)
import utils
import jdecode
import transforms
def main(args):
fds = args.fds
fname = args.fname
block_size = args.block_size
main_seed = args.seed if args.seed != 0 else None
# simple default encoding for now, will add more options with the curriculum
# learning feature
cards = jdecode.mtg_open_file(fname, verbose=True, linetrans=True)
def write_stream(i, fd):
local_random = random.Random(main_seed)
local_random.jumpahead(i)
local_cards = [card for card in cards]
with open('/proc/self/fd/'+str(fd), 'wt') as f:
while True:
local_random.shuffle(local_cards)
for card in local_cards:
f.write(card.encode(randomize_mana=True, randomize_lines=True))
f.write(utils.cardsep)
def mkargs(i, fd):
return i, fd
streaming_noreturn(fds, write_stream, mkargs)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('fds', type=int, nargs='+',
help='file descriptors to write streams to')
parser.add_argument('-f', '--fname', default=os.path.join(libdir, '../data/output.txt'),
help='file to read cards from')
parser.add_argument('-n', '--block_size', type=int, default=10000,
help='number of characters each stream should read/write at a time')
parser.add_argument('-s', '--seed', type=int, default=0,
help='random seed')
args = parser.parse_args()
main(args)

58
scripts/sum.py Executable file
View file

@ -0,0 +1,58 @@
#!/usr/bin/env python
import sys
import os
libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../lib')
sys.path.append(libdir)
def main(fname):
with open(fname, 'rt') as f:
text = f.read()
cardstats = text.split('\n')
nonempty = 0
name_avg = 0
name_dupes = 0
card_avg = 0
card_dupes = 0
for c in cardstats:
fields = c.split('|')
if len(fields) < 4:
continue
nonempty += 1
idx = int(fields[0])
name = str(fields[1])
ndist = float(fields[2])
cdist = float(fields[3])
name_avg += ndist
if ndist == 1.0:
name_dupes += 1
card_avg += cdist
if cdist == 1.0:
card_dupes += 1
name_avg = name_avg / float(nonempty)
card_avg = card_avg / float(nonempty)
print str(nonempty) + ' cards'
print '-- names --'
print 'avg distance: ' + str(name_avg)
print 'num duplicates: ' + str(name_dupes)
print '-- cards --'
print 'avg distance: ' + str(card_avg)
print 'num duplicates: ' + str(card_dupes)
print '----'
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('infile', #nargs='?'. default=None,
help='data file to process')
args = parser.parse_args()
main(args.infile)
exit(0)