shawkinsl/mtga-tracker

View on GitHub
app/parsers.py

Summary

Maintainability
F
1 wk
Test Coverage
import json
import pprint
import datetime
import util
from app.models.game import Game, Match, Player
from app.models.set import Zone
import app.mtga_app
from app.queues import game_state_change_queue, general_output_queue
from app.models.card import Ability
from mtga import all_mtga_cards


@util.debug_log_trace
def parse_jsonrpc_blob(blob):
    pass


@util.debug_log_trace
def parse_get_decklists(blob, version=1):
    import app.mtga_app as mtga_app
    mtga_app.mtga_watch_app.player_decks = {}
    decks = []

    blob_key = "payload"
    if blob_key in blob:
        for deck in blob[blob_key]:
            decks.append(util.process_deck(deck, version=version))

    return decks


@util.debug_log_trace
def parse_update_deck_v3(blob):
    if "payload" in blob:
        return util.process_deck(blob["payload"])


@util.debug_log_trace
def parse_get_player_cards_v3(blob):
    import app.mtga_app as mtga_app
    pass_through("collection", None, blob)
    mtga_app.mtga_watch_app.collection = blob
    mtga_app.mtga_watch_app.save_settings()


@util.debug_log_trace
def pass_through(title, player_key, blob):
    general_output_queue.put({title: blob, "player_key": player_key})


@util.debug_log_trace
def parse_draft_status(blob):
    # TODO: need to implement the sorting algo shown here:
    # TODO: https://github.com/Fugiman/deckmaster/blob/559e3b94bb105387a0e33463e4b5f718ab91721d/client/updater.go#L113

    """ return a.RarityRank() > b.RarityRank() ||
            (a.RarityRank() == b.RarityRank() && a.ColorRank() > b.ColorRank()) ||
            (a.RarityRank() == b.RarityRank() && a.ColorRank() == b.ColorRank() && a.CMC < b.CMC) ||
            (a.RarityRank() == b.RarityRank() && a.ColorRank() == b.ColorRank() && a.CMC == b.CMC && a.Name < b.Name)"""
    import app.mtga_app as mtga_app

    if "payload" not in blob:
        return
    else:
        blob = blob["payload"]

    collection_count = []
    picked_cards_this_draft = []
    if "pickedCards" in blob and blob["pickedCards"]:
        picked_cards_this_draft = blob["pickedCards"]

    if blob["DraftPack"]:
        for card in blob["DraftPack"]:
            card_obj = util.all_mtga_cards.find_one(card).to_serializable()
            if card in mtga_app.mtga_watch_app.collection:
                card_obj["count"] = min(mtga_app.mtga_watch_app.collection[card] + picked_cards_this_draft.count(card), 4)
            else:
                card_obj["count"] = min(0 + picked_cards_this_draft.count(card), 4)
            collection_count.append(card_obj)
        collection_count.sort(key=lambda x: (-1 * util.rank_rarity(x["rarity"]), util.rank_colors(x["color_identity"]), util.rank_cost(x["cost"]), x["pretty_name"]))
        general_output_queue.put({"draft_collection_count": collection_count})
    else:
        blob["DraftPack"] = []

    draftId = blob["DraftId"]
    picks = picked_cards_this_draft[:]
    pack = blob['DraftPack'][:]

    draft_history = mtga_app.mtga_watch_app.draft_history
    if draft_history.get(draftId, None):
        report = {}
        # report['picks'] = [int(grpid) for grpid in draft_history[draftId]['picks'] ]
        report['draftID'] = draftId
        report['playerID'] = blob["playerId"]
        report['pickNumber'] = draft_history[draftId]['picknum']
        report['packNumber'] = draft_history[draftId]['packnum']
        report['pack'] = [int(grpid) for grpid in draft_history[draftId]['pack']]

        old = draft_history[draftId]['picks'][:]
        new = picks[:]
        for c in old:
            new.remove(c)
        report['pick'] = int(new[0])

        # send report to inspector
        app.mtga_app.mtga_logger.info("{}{}".format(util.ld(), report))
        pass_through("draftPick", report["playerID"], report)
    if pack:
        draft_history[draftId] = {'picks': picks, 'pack': pack, 'picknum': blob["pickNumber"], 'packnum': blob["packNumber"]}
    else:
        draft_history[draftId] = None


@util.debug_log_trace
def parse_event_decksubmit(blob, version=1):
    import app.mtga_app as mtga_app
    if "payload" in blob:
        course_deck = blob["payload"]["CourseDeck"]
        if course_deck:
            deck = util.process_deck(course_deck, save_deck=False, version=version)
            mtga_app.mtga_watch_app.intend_to_join_game_with = deck


@util.debug_log_trace
def parse_direct_challenge_queued(blob):
    import app.mtga_app as mtga_app
    course_deck = json.loads(blob["params"]["deck"])
    if course_deck:
        deck = util.process_deck(course_deck, save_deck=False)
        mtga_app.mtga_watch_app.intend_to_join_game_with = deck


@util.debug_log_trace
def parse_sideboard_submit(blob):
    import app.mtga_app as mtga_app
    app.mtga_app.mtga_logger.info("{}".format(pprint.pformat(blob)))
    og_deck_id = mtga_app.mtga_watch_app.intend_to_join_game_with.deck_id
    og_deck_name = mtga_app.mtga_watch_app.intend_to_join_game_with.pool_name

    deck_card_ids = blob['Payload']['SubmitDeckResp']["Deck"]["DeckCards"]
    main_deck_lookup = {}
    for card_id in deck_card_ids:
        if card_id not in main_deck_lookup.keys():
            main_deck_lookup[card_id] = {"id": str(card_id), "quantity": 0}
        main_deck_lookup[card_id]["quantity"] += 1
    new_main_deck_list = [i for i in main_deck_lookup.values()]

    sideboard_card_ids = blob['Payload']['SubmitDeckResp']["Deck"]["SideboardCards"]
    sideboard_lookup = {}
    for card_id in sideboard_card_ids:
        if card_id not in sideboard_lookup.keys():
            sideboard_lookup[card_id] = {"id": str(card_id), "quantity": 0}
        sideboard_lookup[card_id]["quantity"] += 1
    new_sideboard_list = [i for i in sideboard_lookup.values()]

    new_deck_obj = {
        "id": og_deck_id,
        "name": og_deck_name,
        "mainDeck": new_main_deck_list,
        "sideboard": new_sideboard_list
    }
    deck = util.process_deck(new_deck_obj, save_deck=False)
    mtga_app.mtga_watch_app.intend_to_join_game_with = deck


# @util.debug_log_trace
# def parse_event_joinqueue(blob):
#     """ TODO: deprecated? """
#     import app.mtga_app as mtga_app
#     # method = 'Event.JoinQueue'
#     params = blob['params']
#     deckId = params['deckId']  # TODO: this will probably now cause a crash
#     return mtga_app.mtga_watch_app.player_decks[deckId]


def parse_mulligan_req_message(message, timestamp=None):
    import app.mtga_app as mtga_app
    number_cards = message["prompt"]["parameters"][0]["numberValue"]
    player_seat_id = message["systemSeatIds"][0]
    player = mtga_app.mtga_watch_app.game.get_player_in_seat(player_seat_id)
    mtga_app.mtga_logger.info("MULL: {}".format(player.hand.cards))
    with mtga_app.mtga_watch_app.game_lock:  # the game state may become inconsistent in between these steps, so lock it
        if number_cards < 6:
            number_mulligans = 6 - number_cards
            starting_hand_size = 7 - number_mulligans
            player.mulligan_count = number_mulligans
            mulligan_text = [" mulligans to ", str(starting_hand_size), ": "]
        else:  # starting hand
            mulligan_text = ["'s starting hand: "]
        player_texts = build_event_texts_from_iid_or_grpid(player_seat_id, mtga_app.mtga_watch_app.game)
        card_texts = []
        for card in player.hand.cards:
            card_texts.append(*build_event_texts_from_iid_or_grpid(card.game_id, mtga_app.mtga_watch_app.game))
        event_texts = [*player_texts, *mulligan_text]
        for card_text in card_texts:
            event_texts.extend([card_text, ", "])
        if event_texts[-1] == ", ":
            event_texts.pop()
        queue_obj = {"game_history_event": event_texts}
        mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
        general_output_queue.put(queue_obj)


# TODO: move somewhere else
def build_event_text(text, event_type, hover_text=None):
    obj = {"text": text, "type": event_type}
    if hover_text:
        obj["hover"] = hover_text
    return obj


def build_card_event_texts(card, game):
    if isinstance(card, Ability):
        owner_is_hero = game.hero == game.get_player_in_seat(card.owner_seat_id)
        text_type = "{}".format("hero" if owner_is_hero else "opponent")
        ability_source = all_mtga_cards.find_one(card.source_id)
        ability_source_text = build_event_text(ability_source.pretty_name, text_type)
        ability_text = build_event_text("ability", "ability", card.pretty_name)
        card_texts = [ability_source_text, "'s ", ability_text]
    elif isinstance(card, Player):
        text_type = "{}".format("hero" if card == game.hero else "opponent")
        card_texts = [build_event_text(card.player_name, text_type)]
    else:  # it's a GameCard
        owner_is_hero = game.hero == game.get_player_in_seat(card.owner_seat_id)
        text_type = "{}".format("hero" if owner_is_hero else "opponent")
        card_texts = [build_event_text(card.pretty_name, text_type)]
    return card_texts


def build_event_texts_from_iid_or_grpid(iid, game, grpid=None):
    if iid < 3:
        return build_card_event_texts(game.get_player_in_seat(iid), game)
    else:
        card_or_ability = game.find_card_by_iid(iid) or game.find_card_by_iid(grpid)
        if not card_or_ability:
            card_or_ability = all_mtga_cards.find_one(iid)
        return build_card_event_texts(card_or_ability, game)

@util.debug_log_trace
def parse_game_state_message(message, timestamp=None):
    # DOM: ok
    import app.mtga_app as mtga_app
    with mtga_app.mtga_watch_app.game_lock:  # the game state may become inconsistent in between these steps, so lock it
        if "turnInfo" in message.keys():
            if "turnNumber" in message["turnInfo"].keys():
                player = app.mtga_app.mtga_watch_app.game.get_player_in_seat(message["turnInfo"]["activePlayer"])
                if "decisionPlayer" in message["turnInfo"].keys():
                    decisionPlayer = app.mtga_app.mtga_watch_app.game.get_player_in_seat(message["turnInfo"]["decisionPlayer"])
                else:
                    decisionPlayer = app.mtga_app.mtga_watch_app.game.last_decision_player
                if timestamp:
                    now = datetime.datetime.now()
                    if app.mtga_app.mtga_watch_app.game.last_log_timestamp is None:
                        app.mtga_app.mtga_watch_app.game.last_log_timestamp = timestamp
                        app.mtga_app.mtga_watch_app.game.last_measured_timestamp = now
                        app.mtga_app.mtga_watch_app.game.log_start_time = timestamp
                        app.mtga_app.mtga_watch_app.game.last_decision_player = decisionPlayer

                    measured_time_diff = now - app.mtga_app.mtga_watch_app.game.last_measured_timestamp
                    log_time_diff = timestamp - app.mtga_app.mtga_watch_app.game.last_log_timestamp

                    if measured_time_diff > log_time_diff:
                        log_time_diff = measured_time_diff  # some turns are really fast, and the logs see it as 0 seconds. Add what we measured instead,

                    app.mtga_app.mtga_watch_app.game.last_log_timestamp = timestamp
                    app.mtga_app.mtga_watch_app.game.last_measured_timestamp = now
                    ct_obj = {"turnInfo": message["turnInfo"],
                              "diff": log_time_diff,
                              "countsAgainst": app.mtga_app.mtga_watch_app.game.last_decision_player}
                    app.mtga_app.mtga_watch_app.game.chess_timer.append(ct_obj)
                    general_output_queue.put({"decisionPlayerChange": True, "heroIsDeciding": decisionPlayer == app.mtga_app.mtga_watch_app.game.hero})
                    app.mtga_app.mtga_watch_app.game.last_decision_player = decisionPlayer
                app.mtga_app.mtga_watch_app.game.turn_number = message["turnInfo"]["turnNumber"]
                other_player_seat = 2 if message["turnInfo"]["activePlayer"] == 1 else 1
                other_player = app.mtga_app.mtga_watch_app.game.get_player_in_seat(other_player_seat)
                app.mtga_app.mtga_watch_app.game.current_player = player.player_name
                if not app.mtga_app.mtga_watch_app.game.on_the_play:
                    if message["turnInfo"]["turnNumber"] % 2 == 1:
                        app.mtga_app.mtga_watch_app.game.on_the_play = player.player_name
                    else:
                        app.mtga_app.mtga_watch_app.game.on_the_play = other_player.player_name
                app.mtga_app.mtga_watch_app.game.current_phase = message["turnInfo"]["phase"]
                turn_tuple = (message["turnInfo"]["turnNumber"], "phase")
                if turn_tuple not in mtga_app.mtga_watch_app.game.recorded_targetspecs:
                    mtga_app.mtga_watch_app.game.recorded_targetspecs.append(turn_tuple)
                    turn = turn_tuple[0]
                    active_player_seat = message["turnInfo"]["activePlayer"]
                    active_player = mtga_app.mtga_watch_app.game.get_player_in_seat(active_player_seat)
                    if turn % 2 == 1:
                        text = "{} / {} Turn {}".format(turn, active_player.player_name, int((turn + 1) / 2))
                    else:
                        text = "{} / {} Turn {}".format(turn, active_player.player_name, int((turn / 2)))
                    text_obj = build_event_text(text, "turn")
                    queue_obj = {"game_history_event": [text_obj]}
                    mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                    general_output_queue.put(queue_obj)
                if "step" in message["turnInfo"].keys():
                    app.mtga_app.mtga_watch_app.game.current_phase += "-{}".format(message["turnInfo"]["step"])
            app.mtga_app.mtga_logger.debug(message["turnInfo"])
        if 'gameInfo' in message.keys():
            if 'matchState' in message['gameInfo']:
                game_number = message['gameInfo']['gameNumber']
                game_player_id = "-game{}-{}".format(game_number, mtga_app.mtga_watch_app.game.hero.player_id)
                match_id_raw = message['gameInfo']['matchID']
                match_id = message['gameInfo']['matchID'] + game_player_id

                if 'results' in message['gameInfo']:
                    results = message['gameInfo']['results']
                    parse_game_results(True, match_id, results)
                if message['gameInfo']['matchState'] == "MatchState_GameInProgress" and \
                        game_number > max(len(app.mtga_app.mtga_watch_app.match.game_results), 1):
                    shared_battlefield = Zone("battlefield")
                    shared_exile = Zone("exile")
                    shared_limbo = Zone("limbo")
                    shared_stack = Zone("stack")
                    new_hero = Player(mtga_app.mtga_watch_app.game.hero.player_name,
                                      mtga_app.mtga_watch_app.game.hero.player_id,
                                      mtga_app.mtga_watch_app.game.hero.seat,
                                      shared_battlefield, shared_exile, shared_limbo, shared_stack,
                                      mtga_app.mtga_watch_app.game.hero._deck_cards)

                    new_oppo = Player(mtga_app.mtga_watch_app.game.opponent.player_name,
                                      mtga_app.mtga_watch_app.game.opponent.player_id,
                                      mtga_app.mtga_watch_app.game.opponent.seat,
                                      shared_battlefield, shared_exile, shared_limbo, shared_stack,
                                      mtga_app.mtga_watch_app.game.opponent._deck_cards)
                    new_hero.is_hero = True
                    if mtga_app.mtga_watch_app.intend_to_join_game_with:
                        new_hero.original_deck = mtga_app.mtga_watch_app.intend_to_join_game_with
                        new_match_id = match_id_raw + "-game{}-{}".format(game_number, new_hero.player_id)
                        mtga_app.mtga_watch_app.game = Game(new_match_id, new_hero, new_oppo, shared_battlefield,
                                                            shared_exile, shared_limbo, shared_stack,
                                                            app.mtga_app.mtga_watch_app.match.event_id,
                                                            app.mtga_app.mtga_watch_app.match.opponent_rank)
        if 'annotations' in message.keys():
            for annotation in message['annotations']:
                annotation_type = annotation['type'][0]
                if annotation_type == 'AnnotationType_ObjectIdChanged':
                    try:
                        original_id = None
                        new_id = None
                        details = annotation['details']
                        for detail in details:
                            if detail['key'] == "orig_id":
                                original_id = detail["valueInt32"][0]
                                mtga_app.mtga_watch_app.game.ignored_iids.add(original_id)
                                # NOTE: at one point Spencer thought it might be correct to ignore these AFTER
                                # parsing the whole gameStateMessage, i.e. put these in a list here, and only add them
                                # to ignored_iid's at the end of this function.
                                #
                                # That was incorrect, and led to cards flip-flopping in the UI.
                                # This is correct as is.
                            elif detail['key'] == "new_id":
                                new_id = detail["valueInt32"][0]
                        card_with_iid = mtga_app.mtga_watch_app.game.find_card_by_iid(original_id)
                        if not card_with_iid:  # no one has ref'd yet, we don't care
                            continue
                        new_card_already_exists = mtga_app.mtga_watch_app.game.find_card_by_iid(new_id)
                        if new_card_already_exists:  # just wipe the old card, the new card is already there
                            assert new_card_already_exists.mtga_id == card_with_iid.mtga_id or -1 in [new_card_already_exists.mtga_id, card_with_iid.mtga_id], "{} / {}".format(new_card_already_exists.mtga_id , card_with_iid.mtga_id)
                            card_with_iid.mtga_id = -1
                        else:
                            card_with_iid.previous_iids.append(original_id)
                            card_with_iid.game_id = new_id
                    except:
                        app.mtga_app.mtga_logger.error("{}Exception @ count {}".format(util.ld(True), app.mtga_app.mtga_watch_app.error_count))
                        app.mtga_app.mtga_logger.error("{}parsers:parse_game_state_message - error parsing annotation:".format(util.ld(True)))
                        app.mtga_app.mtga_logger.error(pprint.pformat(annotation))
                        app.mtga_app.mtga_watch_app.send_error("Exception during parse AnnotationType_ObjectIdChanged. Check log for more details")
                if annotation_type == "AnnotationType_TargetSpec":
                    affector_id = annotation["affectorId"]
                    affected_ids = annotation["affectedIds"]
                    affector_card = mtga_app.mtga_watch_app.game.find_card_by_iid(affector_id)
                    if not affector_card:
                        # try abilitiy
                        details = annotation["details"]
                        grpid = None
                        for detail in details:
                            if detail["key"] == "grpid":
                                grpid = detail["valueInt32"][0]
                        affector_card = all_mtga_cards.find_one(grpid)
                    targets = []
                    target_texts = []
                    for affected_id in affected_ids:
                        affected_texts = build_event_texts_from_iid_or_grpid(affected_id, mtga_app.mtga_watch_app.game)
                        target_texts.extend(affected_texts)
                        game_obj = mtga_app.mtga_watch_app.game.find_card_by_iid(affected_id)
                        target = game_obj if game_obj else affected_id
                        targets.append(target)
                    if (affector_card, targets) not in mtga_app.mtga_watch_app.game.recorded_targetspecs:
                        mtga_app.mtga_watch_app.game.recorded_targetspecs.append((affector_card, targets))
                        affector_texts = build_card_event_texts(affector_card, mtga_app.mtga_watch_app.game)

                        event_texts = [*affector_texts, " targets "]
                        if len(target_texts) > 2:
                            for target in target_texts:
                                event_texts.extend([target, ", "])
                            event_texts.append(target[-2])
                            event_texts.append(", and")
                            event_texts.append(target[-1])
                        elif len(target_texts) > 1:
                            event_texts.extend([target_texts[-2], " and ", target_texts[-1]])
                        else:
                            event_texts.extend(target_texts)

                        queue_obj = {"game_history_event": event_texts}
                        mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                        general_output_queue.put(queue_obj)
                if annotation_type == "AnnotationType_ResolutionComplete":
                    try:
                        affector_id = annotation["affectorId"]
                        card = mtga_app.mtga_watch_app.game.find_card_by_iid(affector_id)
                        if isinstance(card, Ability):
                            # card resolutions are handled in annotations below
                            __unused_affected_ids = annotation["affectedIds"]
                            grpid = None
                            details = annotation["details"]
                            for detail in details:
                                if detail["key"] == "grpid":
                                    grpid = detail["valueInt32"][0]
                            resolved_texts = build_event_texts_from_iid_or_grpid(affector_id, mtga_app.mtga_watch_app.game, grpid)
                            event_texts = [*resolved_texts, " resolves"]
                            queue_obj = {"game_history_event": event_texts}
                            mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                            general_output_queue.put(queue_obj)
                    except:
                        app.mtga_app.mtga_logger.error("{}Exception @ count {}".format(util.ld(True), app.mtga_app.mtga_watch_app.error_count))
                        app.mtga_app.mtga_logger.error("{}parsers:parse_game_state_message - error parsing annotation:".format(util.ld(True)))
                        app.mtga_app.mtga_logger.error(pprint.pformat(annotation))
                        app.mtga_app.mtga_watch_app.send_error("Exception during parse AnnotationType_ResolutionComplete. Check log for more details")

        if 'gameObjects' in message.keys():
            game_objects = message['gameObjects']
            for object in game_objects:
                card_id = object['grpId']
                instance_id = object['instanceId']
                if instance_id in mtga_app.mtga_watch_app.game.ignored_iids:
                    continue
                owner = object['controllerSeatId']
                type = object["type"]
                zone = object['zoneId']
                if type not in ["GameObjectType_Card", "GameObjectType_Ability", "GameObjectType_SplitCard"]:
                    mtga_app.mtga_watch_app.game.ignored_iids.add(instance_id)
                else:
                    player, zone = mtga_app.mtga_watch_app.game.get_owner_zone_tup(zone)
                    if zone:
                        if not player:
                            player = mtga_app.mtga_watch_app.game.hero
                            # if zone is shared, don't care what player we use to put this card into it
                        assert isinstance(player, Player)
                        if type in ["GameObjectType_Card", "GameObjectType_SplitCard"]:
                            player.put_instance_id_in_zone(instance_id, owner, zone)
                            zone.match_game_id_to_card(instance_id, card_id)
                        elif type == "GameObjectType_Ability":
                            source_instance_id = object['parentId']
                            source_grp_id = object['objectSourceGrpId']
                            ability_name = all_mtga_cards.find_one(card_id)
                            ability = Ability(ability_name, source_grp_id, source_instance_id, card_id, owner, instance_id)
                            zone.abilities.append(ability)
                if "attackState" in object and object["attackState"] == "AttackState_Attacking":
                    card = mtga_app.mtga_watch_app.game.find_card_by_iid(instance_id)
                    limit_tuple = (mtga_app.mtga_watch_app.game.turn_number, "attacks", card)
                    if limit_tuple not in mtga_app.mtga_watch_app.game.recorded_targetspecs:
                        mtga_app.mtga_watch_app.game.recorded_targetspecs.append(limit_tuple)
                        card_texts = build_event_texts_from_iid_or_grpid(instance_id, mtga_app.mtga_watch_app.game)
                        event_texts = [*card_texts, " attacking"]
                        queue_obj = {"game_history_event": event_texts}
                        mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                        general_output_queue.put(queue_obj)
                if "blockState" in object and object["blockState"] == "BlockState_Blocking":
                    card = mtga_app.mtga_watch_app.game.find_card_by_iid(instance_id)
                    block_info = object["blockInfo"]
                    attacker_list = block_info["attackerIds"]
                    for attacker in attacker_list:
                        attacker_card = mtga_app.mtga_watch_app.game.find_card_by_iid(attacker)
                        limit_tuple = (mtga_app.mtga_watch_app.game.turn_number, "blocks", card, attacker_card)
                        if limit_tuple not in mtga_app.mtga_watch_app.game.recorded_targetspecs:
                            mtga_app.mtga_watch_app.game.recorded_targetspecs.append(limit_tuple)
                            attacker_texts = build_event_texts_from_iid_or_grpid(attacker, mtga_app.mtga_watch_app.game)
                            blocker_texts = build_event_texts_from_iid_or_grpid(instance_id, mtga_app.mtga_watch_app.game)

                            event_texts = [*blocker_texts, " blocks ", *attacker_texts]
                            queue_obj = {"game_history_event": event_texts}
                            mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                            general_output_queue.put(queue_obj)
        if 'zones' in message.keys():
            cards_to_remove_from_zones = {}
            for zone in message['zones']:
                try:
                    removable = parse_zone(zone)
                    if removable:
                        cards_to_remove_from_zones[zone["zoneId"]] = removable
                except:
                    app.mtga_app.mtga_logger.error("{}Exception @ count {}".format(util.ld(True), app.mtga_app.mtga_watch_app.error_count))
                    app.mtga_app.mtga_logger.error("{}error parsing zone:".format(util.ld(True)))
                    app.mtga_app.mtga_logger.error(pprint.pformat(zone))
                    app.mtga_app.mtga_watch_app.send_error("Exception during parse zone. Check log for more details")
                    import traceback
                    exc = traceback.format_exc()
                    app.mtga_app.mtga_logger.error(exc)
            for zone_id in cards_to_remove_from_zones.keys():
                remove_these = cards_to_remove_from_zones[zone_id]
                player, zone = mtga_app.mtga_watch_app.game.get_owner_zone_tup(zone_id)
                for card in remove_these:
                    if card in zone.cards:
                        zone.cards.remove(card)
        if message["type"] == "GameStateType_Diff" and "players" in message.keys():
            players = message["players"]
            for player in players:
                seat = player["systemSeatNumber"]
                life_total = player["lifeTotal"]
                player_obj = mtga_app.mtga_watch_app.game.get_player_in_seat(seat)
                if player_obj.current_life_total != life_total:
                    player_is_hero = mtga_app.mtga_watch_app.game.hero == player_obj
                    player_life_text_type = "{}".format("hero" if player_is_hero else "opponent")
                    player_life_text = build_event_text(player_obj.player_name, player_life_text_type)
                    event_texts = [player_life_text, "'s life total changed ", "{} -> {}".format(player_obj.current_life_total, life_total)]
                    queue_obj = {"game_history_event": event_texts}
                    mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                    general_output_queue.put(queue_obj)
                    player_obj.current_life_total = life_total
        # AFTER we've processed gameObjects, look for actions that should go in the log
        # If this code is in the block above gameObjects, then we will end up with lots of
        # "unknown" cards for opponent cards and actions
        if 'annotations' in message.keys():
            for annotation in message['annotations']:
                annotation_type = annotation['type'][0]
                if annotation_type == "AnnotationType_ZoneTransfer":
                    if "affectorId" not in annotation.keys():
                        affector_id = 0
                    else:
                        affector_id = annotation["affectorId"]
                    affected_ids = annotation["affectedIds"]
                    details = annotation["details"]
                    zone_src, zone_dest, category = None, None, None
                    for detail in details:
                        if detail["key"] == "zone_src":
                            zone_src = detail["valueInt32"][0]
                        if detail["key"] == "zone_dest":
                            zone_dest = detail["valueInt32"][0]
                        if detail["key"] == "category":
                            category = detail["valueString"][0]

                    card = mtga_app.mtga_watch_app.game.find_card_by_iid(affected_ids[0])
                    if affector_id == 0:
                        affector_id = card.owner_seat_id
                    player_texts = build_event_texts_from_iid_or_grpid(affector_id, mtga_app.mtga_watch_app.game)
                    annotation_texts = build_event_texts_from_iid_or_grpid(affected_ids[0], mtga_app.mtga_watch_app.game)

                    if category == "PlayLand":
                        event_texts = [*player_texts, " plays ", *annotation_texts]
                        queue_obj = {"game_history_event": event_texts}
                        mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                        general_output_queue.put(queue_obj)
                    elif category == "Draw":
                        if affector_id > 2:
                            owner = card.owner_seat_id
                            player_texts.extend([": ", *build_event_texts_from_iid_or_grpid(owner, mtga_app.mtga_watch_app.game)])
                        if card.pretty_name == "unknown":
                            event_texts = [*player_texts, " draws"]
                        else:
                            event_texts = [*player_texts, " draws ", *annotation_texts]
                        queue_obj = {"game_history_event": event_texts}
                        mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                        general_output_queue.put(queue_obj)
                        # build draw log event
                    elif category == "CastSpell":
                        event_texts = [*player_texts, " casts ", *annotation_texts]
                        queue_obj = {"game_history_event": event_texts}
                        mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                        general_output_queue.put(queue_obj)
                        # build draw log event
                        # TODO: see if this is redundant
                    elif category == "Countered":
                        event_texts = [*player_texts, " counters ", *annotation_texts]
                        queue_obj = {"game_history_event": event_texts}
                        mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                        general_output_queue.put(queue_obj)
                    elif category == "Resolve":
                        event_texts = [*annotation_texts, " resolves"]
                        queue_obj = {"game_history_event": event_texts}
                        mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                        general_output_queue.put(queue_obj)
                    elif category == "Exile":
                        event_texts = [*player_texts, " exiles ", *annotation_texts]
                        queue_obj = {"game_history_event": event_texts}
                        mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                        general_output_queue.put(queue_obj)
                    # TODO: category == "Put" ?
                    elif zone_dest == 37 or zone_dest == 33:  # TODO: get rid of this hardcoded bs
                        event_texts = [*annotation_texts, " sent to graveyard ", "(" + category + ")"]
                        queue_obj = {"game_history_event": event_texts}
                        mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                        general_output_queue.put(queue_obj)

@util.debug_log_trace
def parse_zone(zone_blob):
    import app.mtga_app as mtga_app
    trackable_zones = ["ZoneType_Hand", "ZoneType_Library", "ZoneType_Graveyard", "ZoneType_Exile", "ZoneType_Limbo",
                       "ZoneType_Stack", "ZoneType_Battlefield"]
    zone_type = zone_blob["type"]
    if zone_type not in trackable_zones:
        return []
    owner_seat = None
    mtga_app.mtga_watch_app.game.register_zone(zone_blob)  # make sure we will find the zone later
    zone_id = zone_blob["zoneId"]
    player, zone = mtga_app.mtga_watch_app.game.get_owner_zone_tup(zone_id)
    if not zone:
        if "ownerSeatId" in zone_blob:
            owner_seat = zone_blob["ownerSeatId"]
            player = mtga_app.mtga_watch_app.game.get_player_in_seat(owner_seat)
            zone = player.get_zone_by_name(zone_type)
            zone.zone_id = zone_id
    if zone and not player:
        # we don't care if there is no owner (i.e. a shared zone), we just need a player to reference
        player = mtga_app.mtga_watch_app.game.hero
    if 'objectInstanceIds' in zone_blob:
        for instance_id in zone_blob['objectInstanceIds']:
            if instance_id in mtga_app.mtga_watch_app.game.ignored_iids:
                continue
            if "ownerSeatId" not in zone_blob:
                card = mtga_app.mtga_watch_app.game.find_card_by_iid(instance_id)
                if card:
                    owner_seat = card.owner_seat_id
            if not owner_seat and "ownerSeatId" in zone_blob:
                owner_seat = zone_blob["ownerSeatId"]
            # TODO: logging
            # mtga_logger.info("adding {} to {}".format(instance_id, zone))
            if owner_seat:
                player.put_instance_id_in_zone(instance_id, owner_seat,  zone)
        cards_to_remove_from_zone = []
        for card in zone.cards:
            if card.game_id not in zone_blob['objectInstanceIds']:
                cards_to_remove_from_zone.append(card)
        return cards_to_remove_from_zone


@util.debug_log_trace
def parse_mulligan_response(blob):
    import app.mtga_app as mtga_app
    if blob["mulliganResp"]["decision"] == "MulliganOption_Mulligan":
        player = mtga_app.mtga_watch_app.game.get_player_in_seat(blob["systemSeatId"])
        player.do_mulligan()


@util.debug_log_trace
def parse_accept_hand(blob):
    import app.mtga_app as mtga_app
    client_message = blob['clientToGreMessage']
    response = client_message['mulliganResp']['decision']
    if response == "MulliganOption_AcceptHand":
        with mtga_app.mtga_watch_app.game_lock:
            mtga_app.mtga_watch_app.game.hero.deck.transfer_cards_to(mtga_app.mtga_watch_app.game.temp["my_mulligan"],
                                                                     mtga_app.mtga_watch_app.game.hero.hand)


@util.debug_log_trace
def parse_game_results(_unused_locked, match_id, result_list):
    import app.mtga_app as mtga_app
    for idx, result in enumerate(result_list):
        if not app.mtga_app.mtga_watch_app.match.has_results(idx):
            # scope = result["scope"]
            # if scope == 'MatchScope_Match':  # TODO: with BO3, check games too. (might be in a different event type)
            winning_team = result["winningTeamId"]

            mtga_app.mtga_watch_app.game.final = True
            mtga_app.mtga_watch_app.game.winner = mtga_app.mtga_watch_app.game.get_player_in_seat(winning_team)
            # let electron handle the upload
            result = {
                "match_complete": True,
                "game": mtga_app.mtga_watch_app.game.to_json()
            }

            if "end" not in mtga_app.mtga_watch_app.game.recorded_targetspecs:
                mtga_app.mtga_watch_app.game.recorded_targetspecs.append("end")
                reason = None
                if "reason" in result.keys():
                    reason = result["reason"].split("_")[1]

                won_text = "{} won!".format(mtga_app.mtga_watch_app.game.winner.player_name)
                if reason:
                    won_text += "({})".format(reason)

                event_text = build_event_text(won_text, "game")

                event_texts = [event_text]
                queue_obj = {"game_history_event": event_texts}
                mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])
                general_output_queue.put(queue_obj)

            app.mtga_app.mtga_watch_app.match.add_result(result)
            game_state_change_queue.put(result)
            if match_id != mtga_app.mtga_watch_app.game.match_id:
                fstr = "match_id {} ended, but doesn't match current game object ({})!"
                raise Exception(fstr.format(match_id, mtga_app.mtga_watch_app.game.match_id))


# TODO: turn this back on to track BO3 _event_ result instead of each game
# @util.debug_log_trace
# def parse_match_complete(blob):
#     game_room_info = blob['matchGameRoomStateChangedEvent']['gameRoomInfo']
#     final_match_result = game_room_info['finalMatchResult']
#     result_list = final_match_result["resultList"]
#     match_id = game_room_info['gameRoomConfig']['matchId']
#     parse_game_results(match_id, 1, result_list)


@util.debug_log_trace
def parse_match_created(blob):
    blob = blob["payload"]
    import app.mtga_app as mtga_app
    with mtga_app.mtga_watch_app.game_lock:
        mtga_app.mtga_watch_app.match = Match(blob["matchId"],
                                              blob["eventId"],
                                              blob["opponentScreenName"],
                                              blob["opponentRankingClass"] + " " + str(blob["opponentRankingTier"]))


@util.debug_log_trace
def parse_match_playing(blob):
    # MatchGameRoomStateType_Playing
    import app.mtga_app as mtga_app
    temp_players = {
        1: {},
        2: {}
    }
    game_room_info = blob["matchGameRoomStateChangedEvent"]["gameRoomInfo"]
    event_id = game_room_info['gameRoomConfig']['eventId']
    game_room_players = game_room_info["players"]

    for player in game_room_players:
        temp_players[player["systemSeatId"]]["player_id"] = player["userId"]

    game_room_config = game_room_info["gameRoomConfig"]

    reserved_players = game_room_config["reservedPlayers"]
    for player in reserved_players:
        temp_players[player["systemSeatId"]]["name"] = player["playerName"]

    match_config = game_room_config["matchConfig"]
    if "teams" in match_config:
        teams = match_config["teams"]
        for team in teams:
            players = team["players"]
            for player in players:
                player_seat = player["systemSeatId"]
                temp_players[player_seat]["deck"] = util.card_ids_to_card_list(player["deckCards"])
    for player_idx in [1,2]:
        if "deck" not in temp_players[player_idx]:
            temp_players[player_idx]["deck"] = []
    # set up shared zones
    shared_battlefield = Zone("battlefield")
    shared_exile = Zone("exile")
    shared_limbo = Zone("limbo")
    shared_stack = Zone("stack")
    player1 = Player(temp_players[1]["name"], temp_players[1]["player_id"], 1, shared_battlefield,
                     shared_exile, shared_limbo, shared_stack, temp_players[1]["deck"])
    player2 = Player(temp_players[2]["name"], temp_players[2]["player_id"], 2, shared_battlefield,
                     shared_exile, shared_limbo, shared_stack, temp_players[2]["deck"])
    with mtga_app.mtga_watch_app.game_lock:
        if mtga_app.mtga_watch_app.player_id == player1.player_id:
            hero = player1
            opponent = player2
        elif mtga_app.mtga_watch_app.player_id == player2.player_id:
            hero = player2
            opponent = player1
        else:
            raise Exception("Don't know who hero is: player_id: {} / player 1: {} / player 2: {}".format(mtga_app.mtga_watch_app.player_id, player1.player_id, player2.player_id))
        hero.is_hero = True
        if mtga_app.mtga_watch_app.intend_to_join_game_with:
            hero.original_deck = mtga_app.mtga_watch_app.intend_to_join_game_with
        opponent_rank = "Unknown"
        if mtga_app.mtga_watch_app.match.opponent_name == opponent.player_name:
            opponent_rank = mtga_app.mtga_watch_app.match.opponent_rank
        match_id = game_room_info['gameRoomConfig']['matchId'] + "-game1-{}".format(hero.player_id)

        hero_text = build_event_text(hero.player_name, "hero")
        oppo_text = build_event_text(opponent.player_name, "opponent")

        event_texts = [hero_text, " vs ", oppo_text]
        queue_obj = {"game_history_event": event_texts}
        general_output_queue.put(queue_obj)
        mtga_app.mtga_watch_app.game = Game(match_id, hero, opponent, shared_battlefield, shared_exile, shared_limbo,
                                            shared_stack, event_id, opponent_rank)
        mtga_app.mtga_watch_app.game.events.append(queue_obj["game_history_event"])