def parse_header(header_str):
    """ Parse the header string.

    Return a dictionary with game_end, supply, and resigned fields,
      like parse_game.
    """
    sections = [s for s in header_str.replace(' \n', '\n').split('\n\n') if s]
    end_str, supply_str = sections
    assert 'gone' in end_str or 'resigned' in end_str, "Not gone or resigned"
    if 'gone' in end_str:
        resigned = False
        gone = capture_cards(end_str.split('\n')[1])
    else:
        resigned = True
        gone = []
    supply = indexes(capture_cards(supply_str))
    return {GAME_END: indexes(gone), SUPPLY: supply, RESIGNED: resigned}
def parse_player_start_decks(log_lines):
    start_decks = []
    start_match = PLAYER_AND_START_DECK_RE.match(log_lines[0])
    while start_match:
        line=log_lines.pop(0)
        name = start_match.group(1)
        start_deck = indexes(capture_cards(start_match.group(2)))
        start_decks.append({NAME:name, START_DECK:start_deck})

        start_match = PLAYER_AND_START_DECK_RE.match(log_lines[0])
    return start_decks
def parse_turn(turn_blob, names_list):
    """ Parse the information from a given turn.

    Return a dict containing the following fields.  If any of the fields have
    a value that evaluates to False, do not keep it.

    name: player name.
    number: 1 indexed turn number.
    plays: List of cards played.
    buys: List of cards bought.
    gains: List of cards gained.
    trashes: List of cards trashed.
    returns: List of cards returned.
    ps_tokens: Number of pirate ship tokens gained.
    vp_tokens: Number of victory point tokens gained.
    money: Amount of money available during entire buy phase.
    opp: Dict keyed by opponent index in names_list, containing dicts with trashes/gains.
    """
    lines = turn_blob.strip().split('\n')
    header = lines[0]
    parsed_header = parse_turn_header(header, names_list)

    poss, outpost = False, False

    if 'pname' in parsed_header:
        possessee_name = parsed_header['name']
        possessee_index = names_list.index(possessee_name)
        poss = True
    if 'outpost' in parsed_header:
        outpost = True

    ret = {GAINS: [], TRASHES: [], BUYS: []}
    plays = []
    returns = []
    turn_money = 0
    vp_tokens = 0
    ps_tokens = 0
    opp_turn_info = collections.defaultdict(lambda: {GAINS: [],
                                                     TRASHES: [],
                                                     BUYS: []})
    tracker = PlayerTracker()

    for line_idx, line in enumerate(lines):
        active_player = tracker.get_active_player(line)
        if active_player == tracker.current_player():
            targ_obj = ret
        else:
            # Stop using the player's name here, as it is used as a
            # key name in a dict, which can't be stored in MongoDB if
            # it contains a dot ('.') or starts with a dollar
            # sign. Instead, use the player index number so we can
            # extract the name later.
            #
            # targ_obj = opp_turn_info[names_list[active_player]]
            targ_obj = opp_turn_info[str(active_player)]

        has_trashing = KW_TRASHING in line
        has_trashes = KW_TRASHES in line
        has_gaining = KW_GAINING in line
        orig_buys_len = len(targ_obj.get(BUYS, []))
        orig_gains_len = len(targ_obj.get(GAINS, []))

        did_trading_post_gain = False

        if has_trashes:
            if has_gaining:
                # Trading post turn, first trashes, then gaining
                gain_start = line.find(KW_GAINING)
                targ_obj[TRASHES].extend(capture_cards(line[:gain_start]))
                targ_obj[GAINS].extend(capture_cards(line[gain_start:]))
                did_trading_post_gain = True
            else:
                targ_obj[TRASHES].extend(capture_cards(line))
        if KW_WITH_A in line:
            if KW_REPLACING in line:
                new_gained_portion = line[line.find(KW_WITH_A):]
                targ_obj[GAINS].extend(capture_cards(new_gained_portion))
        if KW_PLAYS in line or KW_PLAYING in line:
            plays.extend(capture_cards(line))
        if has_gaining and not did_trading_post_gain:
            if KW_ANOTHER_ONE in line: # mints a gold gaining another one
                targ_obj[GAINS].extend(capture_cards(line))
            else:
                # gaining always associated with current player?
                targ_obj[GAINS].extend(
                    capture_cards(line[line.find(KW_GAINING):]))
        if KW_BUYS in line:
            targ_obj[BUYS].extend(capture_cards(line))
        if KW_GAINS_THE in line:
            targ_obj[GAINS].extend(capture_cards(line))
        if has_trashing:
            if KW_REVEALS in lines[line_idx - 1] and not KW_DRAWS in line:
                targ_obj[TRASHES].extend(capture_cards(lines[line_idx - 1]))
            if KW_REVEALING in line or KW_REVEALS in line:
                # reveals watchtower trashing ...
                # noble brigand reveals xx, yy and trashes yy
                trashed = capture_cards(line[line.find(KW_TRASHING):])
                targ_obj[TRASHES].extend(trashed)
            else:
                rest = line
                if KW_GAINING in line:
                    rest = line[:line.find(KW_GAINING)]
                targ_obj[TRASHES].extend(capture_cards(rest))
        if KW_GAINS_A in line or KW_GAMES_A in line:
            if KW_TOKEN in line:
                assert get_card('Pirate Ship') in capture_cards(line), 'Pirate ship not in line'
                ps_tokens += 1
            else:
                rest = line[max(line.find(KW_GAINS_A), line.find(KW_GAMES_A)):]
                targ_obj[GAINS].extend(capture_cards(rest))
        if KW_IS_TRASHED in line:
            # Saboteur after revealing cards, name not mentioned on this line.
            cards = capture_cards(line)
            targ_obj[TRASHES].extend(cards)
        if KW_REVEALS in line:
            card_revealed = capture_cards(line)

            # arg, ambassador requires looking at the next line to figure
            # out how many copies were returned
            if (card_revealed and line_idx + 1 < len(lines) and
                KW_RETURNING in lines[line_idx + 1] and not
                KW_REVEALING in lines[line_idx + 1]):
                next_line = lines[line_idx + 1]
                num_copies = 1
                num_copies_match = NUMBER_COPIES.search(next_line)
                if num_copies_match:
                    num_copies = int(num_copies_match.group(1))
                returns.extend(card_revealed * num_copies)
        if KW_REVEALING in line and KW_TO_THE_SUPPLY in line:
            # old style ambassador line
            returns.extend(capture_cards(line))
        if KW_GETTING in line or KW_GETS in line or KW_GET in line:
            money_match = GETTING_MONEY_RE.search(line)
            if money_match:
                turn_money += int(money_match.group(1))
        if KW_WHICH_IS_WORTH in line:
            worth_match = WHICH_IS_WORTH_RE.search(line)
            assert bool(worth_match), line
            turn_money += int(worth_match.group(1))
        if KW_FOR_MONEY in line:
            worth_match = FOR_MONEY_RE.search(line)
            assert bool(worth_match), line
            turn_money += int(worth_match.group(1))
        if u'▼' in line:
            vp_tokens += int(VP_TOKEN_RE.search(line).group('num'))
        if KW_INSTEAD in line and not KW_WISHING in line and 'Trader' in line:
            if 'buy_or_gain' in targ_obj:
                targ_list = targ_obj[targ_obj['buy_or_gain']]
                non_silver_ind = len(targ_list) - 1
                while (non_silver_ind >= 0 and
                       targ_list[non_silver_ind] == 'Silver'):
                    non_silver_ind -= 1
                # This shouldn't work when there is no non-silver, but then
                # non_silver_ind == -1 if there is no non-silver,
                # which magically pops the last item.  <3 guido.
                targ_list.pop(non_silver_ind)
            else:
                assert 'Ill-Gotten Gains' in plays, (
                    "line %s: line\n, targ_obj: %s\n context: %s" % (
                        line, str(targ_obj),
                        '\n'.join(lines[line_idx - 2: line_idx + 2])))

        now_buys_len = len(targ_obj.get(BUYS, []))
        now_gains_len = len(targ_obj.get(GAINS, []))
        if now_buys_len > orig_buys_len:
            targ_obj['buy_or_gain'] = BUYS
        if now_gains_len > orig_gains_len:
            targ_obj['buy_or_gain'] = GAINS

        assert not (now_buys_len > orig_buys_len and
                    now_gains_len > orig_gains_len), 'buys or gains mismatch'

    def _delete_if_exists(d, n):
        if n in d:
            del d[n]

    _delete_if_exists(ret, 'buy_or_gain')

    if poss:
        possessee_info = opp_turn_info[str(possessee_index)]
        for k in [GAINS, TRASHES]:
            _delete_if_exists(possessee_info, k)

        possessee_info[VP_TOKENS], vp_tokens = vp_tokens, 0
        possessee_info[RETURNS], returns = returns, []
        ret[BUYS] = []  # buys handled by possesion gain line.

    for opp in opp_turn_info.keys():
        _delete_if_exists(opp_turn_info[opp], 'buy_or_gain')
        delete_keys_with_empty_vals(opp_turn_info[opp])

        d = opp_turn_info[opp]
        for k, v in d.iteritems():
            if k==VP_TOKENS:
                d[k] = v
            else:
                d[k] = indexes(v)

    ret[BUYS] = indexes(ret[BUYS])
    ret[GAINS] = indexes(ret[GAINS])
    ret[TRASHES] = indexes(ret[TRASHES])

    ret.update({NAME: names_list[tracker.current_player()],
                PLAYS: indexes(plays) , RETURNS: indexes(returns),
                MONEY: count_money(plays) + turn_money,
                VP_TOKENS: vp_tokens, PIRATE_TOKENS: ps_tokens,
                POSSESSION: poss, OUTPOST: outpost,
                OPP: dict(opp_turn_info)})

    delete_keys_with_empty_vals(ret)
    return ret
def parse_turn(log_lines, names_list, trash_pile, trade_route_set, removed_from_supply, masq_targets, previous_name):
    """ Parse the information from a given turn.

    Maintain the trash pile. This is necessary for Forager money counting.
    Maintains the trade route tokens AND the list of all cards gained, which 
    we need to accurately know when Cities are activated.

    Return a dict containing the following fields.  If any of the fields have
    a value that evaluates to False, do not keep it.

    name: player name.
    number: 1 indexed turn number.
    plays: List of cards played.
    buys: List of cards bought.
    gains: List of cards gained.
    trashes: List of cards trashed.
    returns: List of cards returned.
    passes: List of cards passed with Masquerade.
    receives: List of cards received from Masquerade.
    ps_tokens: Number of pirate ship tokens gained.
    vp_tokens: Number of victory point tokens gained.
    money: Amount of money available during entire buy phase.
    opp: Dict keyed by opponent index in names_list, containing dicts with trashes/gains/passes/receives.
    """
    n_players = len(names_list)

    def pile_size(card, n_players):
        if card == dominioncards.Ruins or card == dominioncards.Curse:
            return max((n_players - 1)*10, 10)
        if card == dominioncards.Province:
            if n_players <= 2:
                return 8
            if n_players == 3:
                return 12
            return 12+(n_players - 4)*3
        if card.is_victory():
            if n_players <= 2:
                return 8
            return 12
        if card in [dominioncards.Spoils, dominioncards.Mercenary, 
                dominioncards.Madman] or card.is_ruins() or card.is_shelter():
            return 999 
        if card == dominioncards.Copper:
            if n_players < 5:
                return 60
            return 120
        if card == dominioncards.Silver:
            if n_players < 5:
                return 40
            return 80
        if card == dominioncards.Gold:
            if n_players < 5:
                return 30
            return 60
        if card == dominioncards.Platinum:
            return 12
        if card == dominioncards.Potion:
            return 16
        return 10

    def empty_piles(removed_from_supply, n_players):
        # For cities...
        empty_piles = []

        # First, piles of different cards
        if (sum([removed_from_supply[c] for c in removed_from_supply.keys() if c.is_knight()]) == pile_size(dominioncards.Knights, n_players)):
            empty_piles.append(dominioncards.Knights)
        if (sum([removed_from_supply[c] for c in removed_from_supply.keys() if c.is_ruins()]) == pile_size(dominioncards.Knights, n_players)):
            empty_piles.append(dominioncards.Knights)

        for pile,num in removed_from_supply.items():
            if pile_size(pile, n_players) == num:
                empty_piles.append(pile)
        return empty_piles

    def _delete_if_exists(d, n):
        if n in d:
            del d[n]

    def fix_buys_and_gains(buys, gains):
        """Goko reports each buy and gain separately. This is correct, but
        having everything compatible with iso stats would be nice! So, here
        I 'fix' buys and gains so things which are bought and gained are 
        only reported once, in 'buys', and things which are bought but not
        gained (such as due to trader or possession) are not listed.
        """
        new_buys = []
        for buy in buys:
            if buy in gains:
                gains.remove(buy)
                new_buys.append(buy)
        return (new_buys, gains)

    ret = {PLAYS: [], RETURNS: [], GAINS: [], TRASHES: [], BUYS: [], PASSES: [],
           RECEIVES: []}
    durations = []
    turn_money = 0
    turn_coin_tokens = 0
    vp_tokens = 0
    ps_tokens = 0

    # Keep track of last play, and whether it is still 'active' 
    # for stuff like trashing copper to Moneylender, gaining cards from the
    # trash, etc. Card effects which last more than one line. 
    last_play = None
    harvest_reveal = []
    trashed_to_mercenary = 0
    current_phase = None
    dup_plays_remaining = -1
    done_self_trashing = False
    bom_plays = 0 # For throne room/procession/KC - don't get to rechoose BoM
    bom_choice = None
    bom_processioned = False # to trash it properly
    storeroom_discards = [] 
    done_resolving = True
    coin_tokens = 0

    action_counter = 0 # All this... just for diadem. :/ 

    opp_turn_info = collections.defaultdict(lambda: {GAINS: [], BUYS: [],
                                                     TRASHES: [], PASSES: [],
                                                     RECEIVES:[]})
    while True:
        line = log_lines.pop(0)
        turn_start = START_TURN_RE.match(line)

        if turn_start:
            action_counter = 1
            ret[NAME] = turn_start.group(1)
            ret[NUMBER] = int(turn_start.group(2))
            current_phase = ACTION_PHASE
            if turn_start.group(3):
                ret[POSSESSION] = True
            if previous_name and previous_name not in masq_targets:
                masq_targets[previous_name] = ret[NAME]
            continue

        # empty line ends the turn, clean up and return
        if EMPTY_LINE_RE.match(line):
            # Current goko log bug - does not report 1 VP from Bishop
            vp_tokens += ret[PLAYS].count(dominioncards.Bishop)

            if last_play == dominioncards.Forager and not done_resolving:
                turn_money += sum([d.is_treasure() for d in set(trash_pile)])
            if last_play == dominioncards.Harvest and not done_resolving:
                turn_money += len(set(harvest_reveal))

            money = parse_common.count_money(ret[PLAYS], True) + \
                    turn_money + parse_common.count_money(durations, True) - \
                    durations.count(dominioncards.HorseTraders)*3 

            (buys, gains) = fix_buys_and_gains(ret[BUYS], ret[GAINS])

            ret[BUYS] = indexes(buys)
            ret[PLAYS] = indexes(ret[PLAYS])
            ret[RETURNS] = indexes(ret[RETURNS])
            ret[GAINS] = indexes(gains)
            ret[TRASHES] = indexes(ret[TRASHES])
            ret[PASSES] = indexes(ret[PASSES])
            ret[RECEIVES] = indexes(ret[RECEIVES])
            durations = indexes(durations)
            for opp in opp_turn_info.keys():
                _delete_if_exists(opp_turn_info[opp], 'buy_or_gain')
                parse_common.delete_keys_with_empty_vals(opp_turn_info[opp])

                d = opp_turn_info[opp]
                for k, v in d.iteritems():
                    if k==VP_TOKENS:
                        d[k] = v
                    else:
                        d[k] = indexes(v)

            ret.update({MONEY:money, VP_TOKENS: vp_tokens, 
                PIRATE_TOKENS: ps_tokens, COIN_TOKENS: coin_tokens, OPP: dict(opp_turn_info)})
            return ret



        player_and_rest = HYPHEN_SPLIT_RE.match(line)
        active_player = player_and_rest.group(1)
        action_taken = player_and_rest.group(2)

        # Card-specific processing:
        # Will need to add Stash options whenever stash is implemented.
        # These cards all have unstated effects that last longer than one goko
        # line. For example, Forager will need to count coins after the next
        # 'trash' line - if there is one. Mining Village gives +$2 if trashed - 
        # on the next line after it is played and it draws a card. 
        if (last_play == dominioncards.Forager and not done_resolving and
                KW_TRASHES not in action_taken):
            turn_money += sum([d.is_treasure() for d in set(trash_pile)])
            done_resolving = True
        elif (last_play == dominioncards.MiningVillage and 
                KW_SHUFFLES not in action_taken and
                KW_DRAWS not in action_taken and
                KW_TRASHES not in action_taken):
            done_resolving = True
        elif (last_play == dominioncards.Tournament and
                KW_REVEALS not in action_taken and
                KW_REVEALS_C not in action_taken and
                KW_GAINS not in action_taken):
            done_resolving = True
        elif (last_play == dominioncards.Counterfeit and 
                (KW_PLAYS not in action_taken or
                    dominioncards.Spoils not in capture_cards(action_taken))):
            done_resolving = True
        elif (last_play == dominioncards.Thief and 
                KW_TRASHES not in action_taken and
                KW_SHUFFLES not in action_taken and 
                KW_REVEALS not in action_taken and 
                KW_REVEALS_C not in action_taken and 
                KW_DISCARDS not in action_taken and 
                KW_DISCARDS_C not in action_taken and 
                KW_GAINS not in action_taken):
            done_resolving = True
        elif (last_play == dominioncards.NobleBrigand and 
                KW_GAINS in action_taken and 
                dominioncards.NobleBrigand in capture_cards(action_taken)):
            done_resolving = True
        elif (last_play == dominioncards.NobleBrigand and 
                KW_TRASHES not in action_taken and
                KW_SHUFFLES not in action_taken and 
                KW_REVEALS not in action_taken and 
                KW_REVEALS_C not in action_taken and 
                KW_DISCARDS not in action_taken and 
                KW_DISCARDS_C not in action_taken and 
                KW_GAINS not in action_taken):
            done_resolving = True
        elif (last_play == dominioncards.Rogue and 
                KW_GAINS not in action_taken):
            done_resolving = True
        elif (last_play == dominioncards.Graverobber and 
                KW_GAINS not in action_taken):
            done_resolving = True
        elif (last_play == dominioncards.Mercenary and 
                KW_TRASHES not in action_taken and
                KW_REVEALS not in action_taken and 
                KW_DRAWS not in action_taken and 
                KW_PLACES not in action_taken and 
                KW_GAINS not in action_taken and 
                KW_SHUFFLES not in action_taken):
            done_resolving = True
            trashed_to_mercenary = 0
        elif (last_play == dominioncards.Moneylender and 
                (KW_TRASHES not in action_taken or
                    dominioncards.Copper not in capture_cards(action_taken))):
            done_resolving = True
        elif (last_play == dominioncards.Salvager and 
                KW_TRASHES not in action_taken):
            done_resolving = True
        elif (last_play == dominioncards.Baron and 
                (KW_DISCARDS not in action_taken or 
                    dominioncards.Estate not in capture_cards(action_taken))):
            done_resolving = True
        elif (last_play == dominioncards.Harvest and 
                KW_REVEALS not in action_taken and 
                KW_REVEALS_C not in action_taken and 
                KW_SHUFFLES not in action_taken):
            turn_money += len(set(harvest_reveal))
            harvest_reveal = []
            done_resolving = True
        elif (last_play == dominioncards.BandofMisfits and 
                KW_CHOOSES not in action_taken):
            done_resolving = True
        elif (last_play == dominioncards.Ironmonger and 
                KW_DRAWS not in action_taken and 
                KW_SHUFFLES not in action_taken and 
                KW_REVEALS_C not in action_taken and 
                KW_REVEALS not in action_taken):
            done_resolving = True
        elif (last_play == dominioncards.Ironworks and 
                KW_GAINS not in action_taken):
            done_resolving = True
        elif (last_play == dominioncards.Storeroom and 
                KW_DISCARDS not in action_taken and 
                KW_DRAWS not in action_taken and 
                KW_SHUFFLES not in action_taken):
            turn_money += len(storeroom_discards)
            storeroom_discards = []
            done_resolving = True

        if KW_PLAYS in action_taken:
            played = capture_cards(action_taken)
            ret[PLAYS].extend(played)

            # special cases 
            for play in played: 
                if play.is_action():
                    if not(play == dominioncards.Cultist and play == last_play) and play != dominioncards.BandofMisfits:
                        action_counter -= 1
                action_counter += play.num_plus_actions()
                if bom_choice is not None:
                    if bom_plays == 0:
                        bom_choice = None
                    else:
                        bom_plays -= 1

                if dup_plays_remaining >= 0 and play != dominioncards.BandofMisfits:
                    dup_plays_remaining -= 1
                if dup_plays_remaining < 0:
                    done_self_trashing = False

                if play.is_treasure():
                    phase = BUY_PHASE
                elif (last_play == dominioncards.ThroneRoom or 
                        last_play == dominioncards.Procession or 
                        last_play == dominioncards.Golem):
                    action_counter += 2 
                elif last_play == dominioncards.KingsCourt:
                    action_counter += 3 

                if play == dominioncards.PoorHouse:
                    turn_money += 4 # Subtraction will happen later
                elif play == dominioncards.ThroneRoom or play == dominioncards.Procession:
                    dup_plays_remaining = 2
                elif play == dominioncards.KingsCourt:
                    dup_plays_remaining = 3
                elif play == dominioncards.Madman and dup_plays_remaining <= 0:
                    ret[RETURNS].append(play)
                elif play == dominioncards.Spoils:
                    # Spoils always get returned on play...
                    # ...unless it's Counterfeited. 
                    # Technically, this clause will be incorrect if someone
                    # plays a counterfeit, selects no treasure to counterfeit, 
                    # and then just plays a spoils. 
                    if (last_play != dominioncards.Counterfeit or 
                        done_resolving):
                        ret[RETURNS].append(play)
                elif play == dominioncards.TradeRoute:
                    turn_money += len(trade_route_set)
                elif play == dominioncards.Tournament:
                    turn_money += 1 # Might be canceled out later
                elif play == dominioncards.Diadem:
                    turn_money += action_counter
                elif play == dominioncards.BandofMisfits:
                    bom_processioned = False
                    if last_play == dominioncards.ThroneRoom:
                        bom_plays = 2
                    elif last_play == dominioncards.Procession:
                        bom_plays = 2
                        bom_processioned = True
                    elif last_play == dominioncards.KingsCourt:
                        bom_plays = 3
                    else:
                        bom_plays = 1
                elif play == dominioncards.Conspirator and len(ret[PLAYS]) > 2:
                    action_counter += 1
                elif (play == dominioncards.Crossroads and 
                        ret[PLAYS].count(dominioncards.Crossroads) == 1):
                    action_counter += 3
                elif play == dominioncards.City:
                    if len(empty_piles(removed_from_supply, n_players)) >= 2:
                        turn_money += 1
                    
                last_play = play
                done_resolving = False
            continue

        if KW_BUYS in action_taken:
            buys = capture_cards(action_taken)
            ret[BUYS].extend(buys)

            # easier to deal with the on-buy attack by making it a fake play
            if dominioncards.NobleBrigand in buys:
                last_play == dominioncards.NobleBrigand  
                done_resolving = False
            continue

        if KW_RETURNS in action_taken:
            returned = capture_cards(action_taken)
            ret[RETURNS].extend(returned)
            for r in returned:
                removed_from_supply[r] -= 1
            continue

        if KW_GAINS in action_taken:
            gained = capture_cards(action_taken)
            trade_route_set.update([g for g in gained if g.is_victory()])

            if active_player == ret[NAME]:
                ret[GAINS].extend(gained)
                if(not done_resolving and (last_play == dominioncards.Thief or 
                    last_play == dominioncards.NobleBrigand) and 
                    dominioncards.Mercenary not in gained):
                    for c in gained:
                        if not c.is_treasure():
                            done_resolving = True
                        else:
                            if c in trash_pile:
                                # Early goko logs have bugs with who reported
                                # trashing cards. 
                                trash_pile.remove(c)
                if(not done_resolving and (last_play == dominioncards.Rogue or 
                    last_play == dominioncards.Graverobber) and 
                    dominioncards.Mercenary not in gained):
                    for c in gained:
                        if c in trash_pile:
                            # BoM as death cart is ambiguous
                            trash_pile.remove(c)
                        done_resolving = True
                else:
                    for g in gained:
                        removed_from_supply[g] += 1

                if (last_play == dominioncards.Ironworks and 
                      not done_resolving):
                    if gained[0].is_treasure():
                        turn_money += 1
                    if gained[0].is_action():
                        action_counter += 1
                    done_resolving = True
            else:
                opp_turn_info[str(names_list.index(active_player))][GAINS].extend(gained)
                for g in gained:
                    removed_from_supply[g] += 1
            continue

        # Some old Goko logs mis-attribute trashing from attacks. I'm not 
        # going to special-case all the various goko bugs that have since been
        # fixed, though. So there will be bugs with some old logs.
        if KW_TRASHES in action_taken:
            trashed = capture_cards(action_taken)

            if active_player == ret[NAME]:

                # Making TR-feast not doublecount Feast trashing
                if(last_play not in trashed or not last_play.can_trash_self()):
                    done_self_trashing = False
                if (dup_plays_remaining >= 0 and last_play in trashed and done_self_trashing and last_play.can_trash_self()):
                    trashed.remove(last_play) #TR+feast
                if (last_play in trashed and dup_plays_remaining > 0 and last_play.can_trash_self()):
                    done_self_trashing = True


                if (last_play == dominioncards.MiningVillage and
                    not done_resolving and 
                    dominioncards.MiningVillage in trashed):
                    turn_money += 2
                elif (last_play == dominioncards.Moneylender and 
                      not done_resolving and 
                      dominioncards.Copper in trashed):
                    turn_money += 3
                elif last_play == dominioncards.Salvager and not done_resolving:
                    turn_money += trashed[0].coin_cost
                elif last_play == dominioncards.Mercenary and not done_resolving:
                    trashed_to_mercenary += len(trashed)
                    if trashed_to_mercenary == 2:
                        turn_money += 2
                        done_resolving = True
                        trashed_to_mercenary = 0

                while dominioncards.Fortress in trashed:
                    trashed.remove(dominioncards.Fortress)
                if trashed == bom_choice and (bom_choice[0].can_trash_self() or bom_processioned): 
                    trashed = [dominioncards.BandofMisfits]
                if (bom_choice is not None and
                        dominioncards.TreasureMap in bom_choice and
                        dominioncards.TreasureMap in trashed):
                    trashed.remove(dominioncards.TreasureMap)
                    trashed.extend([dominioncards.BandofMisfits])


                if POSSESSION not in ret:
                    ret[TRASHES].extend(trashed)
                    trash_pile.extend(trashed)

                if last_play == dominioncards.Forager and not done_resolving:
                    turn_money +=sum([d.is_treasure() for d in set(trash_pile)])
                    done_resolving = True

            else:
                while dominioncards.Fortress in trashed:
                    trashed.remove(dominioncards.Fortress)
                opp_turn_info[str(names_list.index(active_player))][TRASHES].extend(trashed)
                trash_pile.extend(trashed)

            if last_play in [dominioncards.MiningVillage, dominioncards.Forager, dominioncards.Salvager]:
                done_resolving = True
            continue

        match = USES_COIN_TOKENS_RE.match(action_taken)
        if match:
            turn_money += int(match.group(1))
            turn_coin_tokens -= int(match.group(1))
            continue

        match = RECEIVES_COIN_TOKENS_RE.match(action_taken)
        if match:
            turn_coin_tokens += int(match.group(1))
            continue

        if KW_PASSES in action_taken:
            passed_cards = capture_cards(action_taken)
            receiver = masq_targets[active_player]

            if active_player == ret[NAME]:
                ret[PASSES].extend(passed_cards)
            else:
                opp_turn_info[str(names_list.index(active_player))][PASSES].extend(passed_cards)
            if receiver == ret[NAME]:
                ret[RECEIVES].extend(passed_cards)
            else:
                opp_turn_info[str(names_list.index(receiver))][RECEIVES].extend(passed_cards)

            continue

        if KW_DURATION in action_taken:
            duration = capture_cards(action_taken)
            durations.extend(duration)
            for d in duration:
                if d in [dominioncards.FishingVillage, dominioncards.Tactician]:
                    action_counter += 1
            continue

        if (KW_CHOOSES_TWO_CARDS_AND_ONE_ACTION in action_taken or 
                KW_RECEIVES_ONE_ACTION in action_taken):
            action_counter += 1
            continue 

        if KW_CHOOSES_TWO_COINS in action_taken:
            turn_money += 2
            continue

        if KW_CHOOSES in action_taken:
            if last_play == dominioncards.BandofMisfits and not done_resolving:
                bom_choice = capture_cards(action_taken)
            if bom_choice[0] == dominioncards.Knights:
                bom_choice[0] == dominioncards.SirMartin
            continue

        if KW_PIRATE_COIN in action_taken:
            ps_tokens += 1
            continue

        if KW_VP_CHIPS in action_taken:
            vp_chips_match = VP_CHIPS_RE.match(action_taken)
            vp_tokens += int(vp_chips_match.group(1))
            continue

        match = TAKES_COINS_RE.match(action_taken)
        if match:
            turn_money += int(match.group(1))
            continue

        match = RECEIVES_COINS_RE.match(action_taken)
        if match:
            turn_money += int(match.group(1))
            continue

        match = TAKES_ACTIONS_RE.match(action_taken)
        if match:
            action_counter += int(match.group(1))
            continue

        match = RECEIVES_ACTIONS_RE.match(action_taken)
        if match:
            action_counter += int(match.group(1))
            continue
        if KW_DISCARDS in action_taken or KW_DISCARDS_C in action_taken:
            if (dominioncards.Estate in capture_cards(action_taken) and 
                    last_play == dominioncards.Baron and 
                    not done_resolving):
                turn_money += 4
                done_resolving = True
            elif (last_play == dominioncards.SecretChamber):
                turn_money += len(capture_cards(action_taken))
            elif (last_play == dominioncards.Vault and 
                    active_player == ret[NAME]):
                turn_money += len(capture_cards(action_taken))
            elif (last_play == dominioncards.Storeroom and not done_resolving):
                storeroom_discards.extend(capture_cards(action_taken))
            continue

        if (KW_REVEALS_HAND in action_taken):
            if last_play == dominioncards.PoorHouse and not done_resolving:
                turn_money -= len([tr for tr in capture_cards(action_taken) if tr.is_treasure()])
                if turn_money < 0:
                    turn_money = 0
            continue


        if (KW_REVEALS in action_taken or KW_REVEALS_C in action_taken):
            if last_play == dominioncards.Harvest:
                harvest_reveal.extend(capture_cards(action_taken))
            elif last_play == dominioncards.Herald:
                c = capture_cards(action_taken)
                if len(c) > 0:
                    if c[0].is_action():
                        action_counter += 1 
                        if bom_plays > 0:
                            bom_plays += 1
                        if dup_plays_remaining > 0:
                            dup_plays_remaining += 1
            elif last_play == dominioncards.Golem:
                action_counter += len([c for c in capture_cards(action_taken)if (c.is_action() and not c == dominioncards.Golem)])
            elif (last_play == dominioncards.Tournament and 
                not done_resolving and active_player != ret[NAME] and 
                dominioncards.Province in capture_cards(action_taken)):
                turn_money -= 1
                done_resolving = True
            elif (last_play == dominioncards.Ironmonger and not done_resolving):
                if capture_cards(action_taken)[0].is_treasure():
                    turn_money += 1
                if capture_cards(action_taken)[0].is_action():
                    action_counter += 1
                done_resolving = True
            continue

        if KW_DRAWS in action_taken: 
            if last_play == dominioncards.Storeroom:
                storeroom_discards = []
            continue
        # All remaining actions should be captured; the next few statements
        # are those which are not logged in any way (though they could be!)

        if (KW_LOOKS_AT in action_taken or 
            KW_RECEIVES in action_taken or 
            KW_PLACES in action_taken or 
            KW_SETS_ASIDE in action_taken or 
            KW_TAKES in action_taken or
            KW_EMBARGOES in action_taken or 
            KW_OVERPAYS in action_taken or 
            KW_NAMES in action_taken or 
            KW_CARDS_IN_DISCARDS in action_taken or
            KW_APPLIED in action_taken or 
            KW_APPLIES_WHEN_TRASHED in action_taken or 
            KW_MOVES in action_taken or 
            KW_MOVES_DECK_TO_DISCARD in action_taken or 
            KW_SCHEME_CHOICE in action_taken or 
            KW_SHUFFLES in action_taken or 
            KW_TAKES_SET_ASIDE in action_taken):
            continue

        raise parse_common.BogusGameError('Line did not match any keywords!')