def chain_zones(olds): """ combines anded zones creating an a single zone :param olds: the current list of tokens :return: list of tokens with zones chained """ news = [] skip = 0 for i,tkn in enumerate(olds): # skip already processed tokens if skip: skip -= 1 continue if tag.is_zone(tkn): # 'and has two cases (1) zone1 and zone2 (2) zone1, zone2, and zone3 # possible to get an (3) and/or ('op<⊕>') try: # case 1 and case 3 if olds[i+1] in ['and','op<⊕>'] and tag.is_zone(olds[i+2]): # untag both zones z1 = tag.untag(tkn)[1] z2 = tag.untag(olds[i+2])[1] # determine the operator to use op = mtgl.AND if olds[i+1] == 'and' else mtgl.AOR news.append(tag.retag('zn',z1+op+z2,{})) skip = 2 continue # case 2 case2 = [mtgl.CMA,tag.is_zone,mtgl.CMA,'and',tag.is_zone] if ll.matchl(olds,case2,stop=1) == 1: zs = [ tag.untag(tkn)[1], tag.untag(olds[i+2])[1], tag.untag(olds[i+5])[1] ] news.append(tag.retag('zn',mtgl.AND.join(zs),{})) skip = 5 continue except IndexError: pass news.append(tkn) return news
def _comma_read_ahead_(tkns): """ reads ahead to determine how a comma will be processed in the current chain :param tkns: the list of tokens :return: mtgl.AND if it is a list of and'ed characteristics, mtgl.OR if it is a list of or'ed characteristcs or ',' if neither """ # read ahead until we find 'or' or 'and' or a non-characteristic for i,tkn in enumerate(tkns): if tkn == 'or': if ll.matchl(tkns[i:],[tag.is_mtg_char,tag.is_mtg_obj],stop=1) == 1: return mtgl.OR else: return mtgl.CMA elif tkn == 'and': return mtgl.AND elif tkn == ',': continue # skip the commas elif not tag.is_mtg_char(tkn): return mtgl.CMA return mtgl.CMA
def parse(txt): """ parses the tokenized text of card into mtgl :param txt: tagged and tokenized text returns the parsed oracle text """ ptxt = [] for i,line in enumerate(txt): try: # check for reference to 'draft' in the line. if so, drop the line if ll.matchl(line,[re_draft]) > -1: continue line = rectify(line) line = chain(line) line = chain_zones(line) line = group(line) line = add_hanging(line) line = merge_possessive(line) ptxt.append(line) except Exception as e: # generic catchall for debugging purposes raise mtgl.MTGLException("Parse failure {} at line {}.".format(type(e),i)) return ptxt
def merge_possessive(olds): """ combines possesive with corresponding object (if there is one) and possessive with corresponding zones (if there is one) :param olds: current list of tokens :return: the updated list of tokens """ news = [] skip = 0 for i,tkn in enumerate(olds): # skip tokens if necessary if skip: skip -= 1 continue if tag.is_mtg_obj(tkn): # after chaining/grouping, objects that have possesives # will be follwed by xp<player> xc<[not]control|own> if ll.matchl(olds[i:],[tag.is_player,is_possessive],stop=1) == 1: ot,ov,op = tag.untag(tkn) # object tag pt,pv,pp = tag.untag(olds[i+1]) # player tag _,v,_ = tag.untag(olds[i+2]) # possessive tag # is the possessive negated? if so, strip it out neg = mtgl.NOT in v if neg: v = v[1:] # determine property prop = 'controller' if v == 'control' else 'owner' # process 'you' and player/opponent differently if pv == 'you': if neg: pv = 'opponent' # make opponent if negated op[prop] = pv news.append(tag.retag(ot,ov,op)) else: # if there is a quantifier, AND it with player if not 'quantifier' in pp: val = pv else: val = pp['quantifier'] + mtgl.AND + pv # does the player have a status? # TODO: 1. haven't seen any players with a status and # quantifier 2. several ways to add status, could use # AND or ARW or wrap in parenthesis etc - which is best? if 'status' in pp: val = pp['status'] + mtgl.AND + val op[prop] = val news.append(tag.retag(ot,ov,op)) # skip the merged tokens skip = 2 continue elif tag.is_player(tkn): # after chaining/grouping, players that possess a zone will be # followed by the zone try: if tag.is_zone(olds[i+1]): pt,pv,pps = tag.untag(tkn) zt,zv,_ = tag.untag(olds[i+1]) # shouldnt be a prop-list # check for quantifiers in the plaer if not 'quantifier' in pps: val = pv else: val = pps['quantifier'] + mtgl.AND + pv # check for 'of' in the player (looking for xp<owner of=it> if 'of' in pps: val = pps['of'] + mtgl.ARW + val # add the zone to news news.append(tag.retag(zt,zv,{'player':val})) skip = 1 # skip the next token (the zone) continue except IndexError: pass # no possessive, just append the token news.append(tkn) return news
def add_hanging(olds): """ adds any hanging meta characterisitics, status to an object and the seq. pr<in> zn<ZONE> to objects :param olds: the current mtgl :return: updated mtgl w/ hanging meta characteristics, status added to objs """ news = [] skip = 0 for i,tkn in enumerate(olds): # skip already processed tokens if skip: skip -= 1 continue if tkn == 'pr<with>' or tkn == 'pr<without>': # if the last token added is an object untag it if news and tag.is_mtg_obj(news[-1]): ot,ov,ops = tag.untag(news[-1]) # two possibilities following the with/without # keywords - the next 'phrase' is "kw<KW>" or "kw<KW> and kw<KW>" # we assume there won't be 'or'ed keywords or more than 2 # NOTE: we have to check the double kw first # meta - the next 'phrase' is ch<META> op<OP> nu<NU> skw = [tag.is_keyword] dkw = [tag.is_keyword,'and',tag.is_keyword] meta = [tag.is_meta_char,tag.is_operator,tag.is_number] if ll.matchl(olds[i+1:],dkw,stop=0) == 0: assert(tkn == 'pr<with>') # shouldn't have a neg in this case # pull out both keywords and combine kw = mtgl.AND.join( [tag.untag(olds[i+1])[1],tag.untag(olds[i+3])[1]] ) # add the keyword to the objects meta or create meta parameter if 'meta' in ops: ops['meta'] += mtgl.AND + kw else: ops['meta'] = kw # retags and appends the popped object) news.pop() news.append(tag.retag(ot,ov,ops)) skip = 3 continue elif ll.matchl(olds[i+1:],skw,stop=0) == 0: # single keyword, extract it, & check if should be negative kw = tag.untag(olds[i+1])[1] neg = '' if tkn == 'pr<with>' else mtgl.NOT # add the keyword to the objects meta or create meta parameter if 'meta' in ops: ops['meta'] += mtgl.AND + neg + kw else: ops['meta'] = kw # retags and appends the popped object) news.pop() news.append(tag.retag(ot,ov,ops)) skip = 1 continue elif ll.matchl(olds[i+1],meta,stop=0) == 0: # 2) meta op number mv = tag.untag(olds[i+1])[1] # untag the meta characteristic ov = tag.untag(olds[i+2])[1] # and the operator nv= tag.untag(olds[i+3])[1] # and the number # check if obj already has a meta parameter if 'meta' in ops: ops['meta'] += mtgl.AND + mv + ov + nv else: ops['meta'] = mv + ov + nv # retags and appends the popped object) news.pop() news.append(tag.retag(ot,ov,ops)) skip = 3 continue # if nothing happens, the preposition will fall through and # be added elif tag.is_state(tkn): # untag the last token added and verify it's an object try: t,v,p = tag.untag(news[-1]) if t == 'ob': # untag the current, saving the 'status' value ss = [tag.untag(tkn)[1]] op = mtgl.AND # read ahead while and/or and status are present j = i+1 while True: try: if tag.is_state(olds[j]): ss.append(tag.untag(olds[j])[1]) skip += 1 j += 1 elif olds[j] == 'or': op = mtgl.OR skip += 1 j += 1 elif olds[j] == 'and': skip += 1 j += 1 else: break except IndexError: break # add the status(es) if 'status' in p: p['status'] += op.join(ss) else: p['status'] = op.join(ss) news.pop() news.append(tag.retag(t,v,p)) continue except mtgl.MTGLTagException: pass elif tkn == 'xc<devotion>': # look for devotion 'clauses' - devotion will be of the form: # xp<PLY> xc<devotion> pr<to> ob<card characteristics=CH> # and although currently worded only for xp<you>, we will consider # any player as viable try: # last added will be xp, next 2 will be pr<to> and ch<...> if tag.is_player(news[-1]) and olds[i+1] == 'pr<to>' and\ tag.is_mtg_obj(olds[i+2]): # for devotion, player should be simple & characteristics # should be a color(s) ply = tag.untag(news.pop())[1] # should always be 'you' ps = tag.untag(olds[i+2])[2] news.append( tag.retag('xp',ply,{'devotion':ps['characteristics']}) ) skip = 2 continue except mtgl.MTGLTagException: pass except (IndexError,ValueError): pass elif tag.is_zone(tkn): # TODO: make sure this is working as expected # check if we have a phrase ob<OBJECT> pr<in> zn<ZONE> try: if tag.is_mtg_obj(news[-2]) and news[-1] == 'pr<in>': # untag the zone and get owner or quantifer t,v,ps = tag.untag(tkn) assert(not('quantifier' in ps and 'player' in ps)) p = None if 'quantifier' in ps: p = ps['quantifier'] elif 'player' in ps: p = ps['player'] # untag the existing object (make it zone->player or # zone->quantifier) and add to that object news.pop() # pop the preposition ot,ov,ops = tag.untag(news.pop()) if p: ops['zone'] = '{}{}{}'.format(v,mtgl.ARW,p) else: ops['zone'] = v # add the ob back to news news.append(tag.retag(ot,ov,ops)) continue except IndexError: pass news.append(tkn) return news
def rectify(olds): """ fixes several issues. 1. token (non)token is the only top-level object from rule 109 but acts like a a chracteristic. 2. or'ed and and'ed objects (primarily/only spells or abilities) need to be combined 3. double tagged an mis-tagged require context to be fixed 4. combines phrases like nu<1> or more or nu<10> or less into a single nu tag 5. miscellaneous fixes that assist in parsing and graphing :param olds: the current list of tokens :return: tokens with subsequent entities merged """ # 5. before enumerating the tokens we'll make some list replacements # a. combined or'ed activated, triggered status (see Stifle olds = ll.replacel( olds,['xs<activated>','or','xs<triggered>'],['xs<activated∨triggered>'] ) # b. combine xo<it> xp<...> and xp<their> xp<...> i = ll.matchl(olds,[ll.ors('xo<it>','xp<their>'),tag.is_player]) while i > -1: t,v,ps = tag.untag(olds[i+1]) ps['of'] = 'it' if olds[i] == 'xo<it>' else 'them' # add of=? to proplist olds[i:i+2] = [tag.retag(t,v,ps)] # retag i = ll.matchl(olds,[ll.ors('xo<it>', 'xp<their>'),tag.is_player]) news = [] skip = 0 for i,tkn in enumerate(olds): # skip any tokens that have already been processed if skip: skip -= 1 continue # check 1 and 2 here if current token is an object if tag.is_mtg_obj(tkn): # (1) move (non)token to right of characteristics if 'token' in tkn and ll.matchl(olds[i:],[tag.is_mtg_char],stop=1) == 1: j = i+1 while ll.matchl(olds[j:],[tag.is_mtg_char],stop=0) == 0: news.append(olds[j]) j += 1 skip = (j-i)-1 news.append(tkn) continue # (2) make two and'ed/or'ed 'consecutive' singleton objs one if ll.matchl(olds[i:],[tag.is_coordinator,tag.is_mtg_obj],stop=1) == 1: # see Glyph Keeper (spell or ability) t,v,p = tag.untag(olds[i+2]) if not p: # don't join anything with a prop list if olds[i+1] == 'or': op = mtgl.OR elif olds[i+1] == 'and': op = mtgl.AND else: op = mtgl.AOR news.append(tag.retag( t,"{}{}{}".format(tag.untag(tkn)[1],op,v),{}) ) skip = 2 continue # (3) determine if # a) 'copy' is always tagged as an ob because lituus actions check for an # existing tag before tagging. If it's followed by an object # or quantifier, then its an action (lituus) otherwise its an object if tkn == 'ob<copy>': # copy quantifier spell|ability, copy quantifier instant|sorcery if ll.matchl(olds[i:],[tag.is_quantifier,tag.is_mtg_obj],stop=1) == 1: news.append('xa<copy>') elif ll.matchl(olds[i:],[tag.is_quantifier,tag.is_mtg_char],stop=1) == 1: news.append('xa<copy>') elif ll.matchl(olds[i:],['xo<it>'],stop=1) == 1: # i.e. copy it news.append('xa<copy>') else: # last check, looking for a phrase "copy the ob<...>" where the # value of ob is spell. Also looking for one of two phrases # in the already added tokens 'you may' or 'spell,' j = ll.matchl(news,['xp<you>','cn<may>']) k = ll.matchl(news,['ob<spell>',mtgl.CMA]) if j > -1 and i-j == 2: news.append('xa<copy>') elif k > -1 and i-k == 2: news.append('xa<copy>') elif ll.matchl(olds[i:],['xq<the>',tag.is_mtg_obj],stop=1) == 1: _,v,ps = tag.untag(olds[i+2]) if v == 'spell': news.append('xa<copy>') else: news.append('ob<copy>') else: news.append('ob<copy>') continue # b) target (114) if tkn == 'xq<target>': # target could be a: # 1. action 'that targets one or more creatures' # 2. quantifier 'destroy target creature' # 3. object 'can't be the target of spells or abilities' # Although as an object, it presence in non-reminder text is limtited # NOTE: by using the slice "-n:" we avoid index errors if ll.matchl(news[-3:],['cn<cannot>','be','xq<the>']) == 0: # check conversion to object first. The full phrase is "cannot be # the target of..." but we can check news for "cannot be the". news.append('xo<target>') elif ll.matchl(news[-2:],[ll.ors('becomes','change'),'xq<the>']) == 0: # another conversion to object requires checking for 'becomes' or # 'change' prior to the news.append('xo<target>') elif ll.matchl(news[-3:],['pr<with>','a','single']) == 0: # look for "with a single" # TODO: are there cases where 'single' is not present or different news.append('xo<target>') elif ll.matchl(news[-1:],[ll.ors('new','xa<any>')]) == 0: # last object check if the preceding word is new news.append('xo<target>') elif ll.matchl(news[-1:],[ll.ors('xq<that>','could','ob<copy>','must')]) == 0: # determine if 'target' is an action, the easiest is to check the # preceding tokens if we find, 'that', 'could', 'copy', must it's # an action NOTE: based on the assumption that rectify has correctly # fixed copy tokens news.append('xa<target>') else: news.append('xq<target>') continue # c) ka<exile> is an action or a zone. If its preceded by a preposition # change it to zone, otherwise leave it alone if tkn == 'ka<exile>': try: if tag.is_preposition(news[-1]): news.append('zn<exile>') else: news.append(tkn) except IndexError: news.append(tkn) continue # d) ka<counter> is a keyword action or refers to a counter. In counter # spells, the word counter is followed by a quantifier. Additionally, # the phrase cannot be countered (cn<cannot>, be, ka<counter>) referes # to the keyword action if tkn == 'ka<counter>': try: if tag.is_quantifier(olds[i+1]) or\ news[i-2:i] == ['cn<cannot>','be']: news.append(tkn) else: news.append('xo<ctr>') except IndexError: news.append('xo<ctr>') continue # e) ka<vote> vote appears often in cards relating to voting. We could # take the "strict" interpretation but since we don't yet do that for # other keyword-actions we will change cases of the form # "tied for the most votes", "gets more votes", "vote is tied" # TODO: it doesn't catch for each x vote see Expropriate if tkn == 'ka<vote>': try: if news and news[-1] == 'most': news.append('vote') elif news and news[-1] == 'more': news.append('vote') elif olds[i+1:i+3] == ['is','tied']: news.append('vote') else: news.append(tkn) except IndexError: news.append('vote') continue # f) activate/trigger mtgl and tagger do remove conjugations from activated # (& triggered) so we end up with both 'activate' and 'activated' where # activated is tagged as a status. see Cursed Totem. Here the first activated # is a status but the last activated should be rectified to an action if tkn == 'xs<activated>' or tkn == 'xs<triggered>': # create the tag and value if we have to change the token if tkn == 'xs<activated>': t = 'ka' # activate is recognized in the rules as a KWA v = 'activate' else: t = 'xa' # trigger is not recognized v = 'trigger' # if the next token is an object we have a status otherwise, we should # retag as an action if ll.matchl(olds[i:],[tag.is_mtg_obj],stop=1) != 1: news.append(tag.retag(t,v,{})) else: news.append(tkn) continue # (4) combine numeric phrases if tag.is_number(tkn): # check for 'or' followed by 'more' or 'less' try: op = None if ll.matchl(olds[i+1:],['or','less']) == 0: op = mtgl.LE elif ll.matchl(olds[i+1:],['or','more']) == 0: op = mtgl.GE if op: news.append("nu<{}{}>".format(op,tag.untag(tkn)[1])) skip = 2 continue except IndexError: pass # append the token news.append(tkn) return news
def _chain_pt_(ns): if ll.matchl(ns,[tag.is_lituus_act],start=len(ns)-1) > -1: return False if ll.matchl(ns,['choice','of']) > -1: return False return True
def chain(olds): """ combines charcteristics belonging to an object, creating an implied object if necessary - characteristics will occur prior to the object (if there is one) Also takes care of card name card like phrases (see Hanweir Battlements) :param olds: the current list of tokens :return: chained list of tokens NOTE: Perceived "rules" regarding and'ed, or'ed sequences of characterisitics along with uses of commas in MTG oracle text are: 1. chained characteristics are always either all 'anded' or all 'ored' 2. chained characteristics will precede the object (if there is one) a. token, while an object (109), acts as a characterisitic 3. 'and' chains are (almost always) space delimited a. any comma signifies distinct objects vice a sequence b. two items may be 'and'ed if there are no commas c. two characteristics may be comma 'and'ed if they are followed by a object i.e. type, type obj and the types are negated 4. 'or' chains will contain the word 'or' and will be comma delimited for chains of more than 2 characterisitcs """ news = [] # new tokens cs = [] # list of characterisitcs pt = None # power/toughness op = mtgl.AND # the chain operator ti = 'ob' # tag-id tp = "characteristics" # prop-list key for i,tkn in enumerate(olds): if tag.is_mtg_char(tkn): # only chain meta characteristics if they are p/t & meet specific criteria _,v,p = tag.untag(tkn) if not tag.is_meta_char(tkn): cs.append(v) else: # if we find a lituus action i.e. gets or the word of, do # not add if v == 'p/t' and _chain_pt_(news): pt = p['val'] else: # close out the chain if its open if cs: pl = {tp:op.join(cs)} if pt: pl['meta'] = 'p/t' + mtgl.EQ + pt news.append(tag.retag(ti,_implied_obj_(cs),pl)) op = mtgl.AND cs = [] pt = None # then add the token news.append(tkn) else: # current token is not a characterisitic. If we're not in a chain, # append the token otherwise determine how to treat the token if not cs: news.append(tkn) # not in chain, append the token else: # we are in a current chain if tkn == ',': nop = _comma_read_ahead_(olds[i:]) # see grapher->collate, the rules for determining if objects # should be chained is cumbersome. we'll use basic hueristics # here and allow the grapher to apply the additional rules if nop == mtgl.AND or nop == ',': # distinct objects pl = {tp:op.join(cs)} if pt: pl['meta'] = 'p/t'+mtgl.EQ+pt news.append(tag.retag(ti,_implied_obj_(cs),pl)) news.append(',') op = mtgl.AND cs = [] pt = None else: op = mtgl.OR elif tkn == 'or': op = mtgl.OR elif tkn == 'and': # have to read ahead. if next token is not a characteristic, # need to close out the current chain, and append the 'and' # otherwise just set the op and continue if _chained_and_(i,olds): op = mtgl.AND else: pl = {tp:op.join(cs)} if pt: pl['meta'] = 'p/t'+mtgl.EQ+pt news.append(tag.retag(ti,_implied_obj_(cs),pl)) news.append(tkn) op = mtgl.AND cs = [] pt = None elif tag.is_mtg_obj(tkn): t,v,p = tag.untag(tkn) # untag it p[tp] = op.join(cs) # update it's prop list if pt: p['meta'] = 'p/t'+mtgl.EQ+pt # & meta if present news.append(tag.retag(t,v,p)) # retag and append op = mtgl.AND # and reset the chain cs = [] pt = None else: # no object, end the chain and append the implied object pl = {tp:op.join(cs)} if pt: pl['meta'] = 'p/t'+mtgl.EQ+pt news.append(tag.retag(ti,_implied_obj_(cs),pl)) news.append(tkn) op = mtgl.AND cs = [] pt = None # if unbuilt chain remains, build/append the object then return the new tkns # p/t should never be present if other characteristics are not present if cs: pl = {tp:mtgl.AND.join(cs)} if pt: pl['meta'] = 'p/t'+mtgl.EQ+pt news.append(tag.retag(ti,_implied_obj_(cs),pl)) # combine card named card and variants. Looking for ob<card> name ob<card...> # this phrase will be combined into a single tagged token. This is performed # here because characteristics will have been tagged as an object (Hanweir # Battlements' 'creature named ...' at this point is now ob<permanent # characteristics=creature> named ..." which is easier to process. Additionaly, # if not done here it will be much harder to later cnc = [tag.is_mtg_obj,'named',tag.is_mtg_obj] i = ll.matchl(news,cnc) while i > -1: # the first token should be a permanent with a type in the characteristics # attribute. if not, it should be ob<card> t1,v1,ps1 = tag.untag(news[i]) ctype = ps1['characteristics'] if 'characteristics' in ps1 else None # the last token should be a ob<card...> with a ref attribute t2,v2,ps2 = tag.untag(news[i+2]) # create a new token using the val from the first, the prop list from # the second (adding characterstics from the first one) if ctype: # shouldn't be chcaracteristics in the 2nd object but just in case try: ps2['characteristics'] += mtgl.AND + ctype except KeyError: ps2['characteristics'] = ctype news[i:i+3] = [tag.retag(t1,v1,ps2)] # check for more i = ll.matchl(news,cnc) return news