Beispiel #1
0
    def test_parse_called_chakan(self):
        decoder = TenhouDecoder()
        meld = decoder.parse_meld('<N who="3" m="18547" />')

        self.assertEqual(meld.who, 3)
        self.assertEqual(meld.type, Meld.CHAKAN)
        self.assertEqual(meld.tiles, [48, 49, 50, 51])
Beispiel #2
0
    def test_parse_called_chi(self):
        decoder = TenhouDecoder()
        meld = decoder.parse_meld('<N who="3" m="27031" />')

        self.assertEqual(meld.who, 3)
        self.assertEqual(meld.type, Meld.CHI)
        self.assertEqual(meld.tiles, [42, 44, 51])
Beispiel #3
0
    def test_parse_called_kan(self):
        decoder = TenhouDecoder()
        meld = decoder.parse_meld('<N who="3" m="13825" />')

        self.assertEqual(meld.who, 3)
        self.assertEqual(meld.type, Meld.KAN)
        self.assertEqual(meld.tiles, [52, 53, 54, 55])
Beispiel #4
0
    def test_parse_called_pon(self):
        decoder = TenhouDecoder()
        meld = decoder.parse_meld('<N who="3" m="34314" />')

        self.assertEqual(meld.who, 3)
        self.assertEqual(meld.type, Meld.PON)
        self.assertEqual(meld.tiles, [89, 90, 91])
    def test_parse_called_chi(self):
        decoder = TenhouDecoder()
        meld = decoder.parse_meld('<N who="3" m="27031" />')

        self.assertEqual(meld.who, 3)
        self.assertEqual(meld.type, Meld.CHI)
        self.assertEqual(meld.opened, True)
        self.assertEqual(meld.tiles, [42, 44, 51])
    def test_parse_called_chakan(self):
        decoder = TenhouDecoder()
        meld = decoder.parse_meld('<N who="3" m="18547" />')

        self.assertEqual(meld.who, 3)
        self.assertEqual(meld.type, Meld.CHANKAN)
        self.assertEqual(meld.opened, True)
        self.assertEqual(meld.tiles, [48, 49, 50, 51])
    def test_parse_called_pon(self):
        decoder = TenhouDecoder()
        meld = decoder.parse_meld('<N who="3" m="34314" />')

        self.assertEqual(meld.who, 3)
        self.assertEqual(meld.type, Meld.PON)
        self.assertEqual(meld.opened, True)
        self.assertEqual(meld.tiles, [89, 90, 91])
def test_parse_called_chakan():
    decoder = TenhouDecoder()
    meld = decoder.parse_meld('<N who="3" m="18547" />')

    assert meld.who == 3
    assert meld.type == MeldPrint.SHOUMINKAN
    assert meld.opened is True
    assert meld.tiles == [48, 49, 50, 51]
def test_parse_called_chi():
    decoder = TenhouDecoder()
    meld = decoder.parse_meld('<N who="3" m="27031" />')

    assert meld.who == 3
    assert meld.type == MeldPrint.CHI
    assert meld.opened is True
    assert meld.tiles == [42, 44, 51]
def test_parse_called_pon():
    decoder = TenhouDecoder()
    meld = decoder.parse_meld('<N who="3" m="34314" />')

    assert meld.who == 3
    assert meld.type == MeldPrint.PON
    assert meld.opened is True
    assert meld.tiles == [89, 90, 91]
def test_parse_called_opened_kan():
    decoder = TenhouDecoder()
    meld = decoder.parse_meld('<N who="3" m="13825" />')

    assert meld.who == 3
    assert meld.from_who == 0
    assert meld.type == MeldPrint.KAN
    assert meld.opened is True
    assert meld.tiles == [52, 53, 54, 55]
def test_parse_called_closed_kan():
    decoder = TenhouDecoder()
    meld = decoder.parse_meld('<N who="0" m="15872" />')

    assert meld.who == 0
    assert meld.from_who == 0
    assert meld.type == MeldPrint.KAN
    assert meld.opened is False
    assert meld.tiles == [60, 61, 62, 63]
    def test_parse_called_closed_kan(self):
        decoder = TenhouDecoder()
        meld = decoder.parse_meld('<N who="0" m="15872" />')

        self.assertEqual(meld.who, 0)
        self.assertEqual(meld.from_who, 0)
        self.assertEqual(meld.type, Meld.KAN)
        self.assertEqual(meld.opened, False)
        self.assertEqual(meld.tiles, [60, 61, 62, 63])
    def test_parse_called_opened_kan(self):
        decoder = TenhouDecoder()
        meld = decoder.parse_meld('<N who="3" m="13825" />')

        self.assertEqual(meld.who, 3)
        self.assertEqual(meld.from_who, 0)
        self.assertEqual(meld.type, Meld.KAN)
        self.assertEqual(meld.opened, True)
        self.assertEqual(meld.tiles, [52, 53, 54, 55])
    def test_parse_called_closed_kan(self):
        decoder = TenhouDecoder()
        meld = decoder.parse_meld('<N who="0" m="15872" />')

        self.assertEqual(meld.who, 0)
        self.assertEqual(meld.from_who, 0)
        self.assertEqual(meld.type, Meld.KAN)
        self.assertEqual(meld.opened, False)
        self.assertEqual(meld.tiles, [60, 61, 62, 63])
class TenhouLogReproducer(object):
    """
    The way to debug bot decisions that it made in real tenhou.net games
    """
    def __init__(self, mjlog_file=None, log_url=None, stop_tag=None):
        if log_url:
            log_id, player_position, needed_round = self._parse_url(log_url)
            log_content = self._download_log_content(log_id)
        elif mjlog_file:
            with open(mjlog_file, encoding="utf8") as f:
                log_id = mjlog_file.split("/")[-1].split(".")[0]
                player_position = 0  # tw: seat
                needed_round = 1  # ts: round
                log_content = f.read()
        rounds = self._parse_rounds(log_content)

        self.player_position = player_position
        self.round_content = rounds[needed_round]
        self.stop_tag = stop_tag
        self.decoder = TenhouDecoder()

        # ADD: to get results of all rounds
        self.rounds = rounds
        # ADD: to extract features to be saved
        self.extract_features = ExtractFeatures()

    def reproduce(self, dry_run=False):
        draw_tags = ['T', 'U', 'V', 'W']
        discard_tags = ['D', 'E', 'F', 'G']

        player_draw = draw_tags[self.player_position]

        player_draw_regex = re.compile('^<[{}]+\d*'.format(
            ''.join(player_draw)))

        draw_regex = re.compile('^<[{}]+\d*'.format(''.join(draw_tags)))
        discard_regex = re.compile('^<[{}]+\d*'.format(''.join(discard_tags)))

        table = Table()
        previous_tag = ""
        score = 1
        is_valid_sample = False
        for n, tag in enumerate(self.round_content):
            if dry_run:
                print(tag)

            if not dry_run and tag == self.stop_tag:
                break

            if 'INIT' in tag:
                values = self.decoder.parse_initial_values(tag)

                shifted_scores = []
                for x in range(0, 4):
                    shifted_scores.append(
                        values['scores'][self._normalize_position(
                            x, self.player_position)])

                table.init_round(
                    values['round_number'],
                    values['count_of_honba_sticks'],
                    values['count_of_riichi_sticks'],
                    values['dora_indicator'],
                    self._normalize_position(self.player_position,
                                             values['dealer']),
                    shifted_scores,
                )

                hands = [
                    [
                        int(x) for x in self.decoder.get_attribute_content(
                            tag, 'hai0').split(',')
                    ],
                    [
                        int(x) for x in self.decoder.get_attribute_content(
                            tag, 'hai1').split(',')
                    ],
                    [
                        int(x) for x in self.decoder.get_attribute_content(
                            tag, 'hai2').split(',')
                    ],
                    [
                        int(x) for x in self.decoder.get_attribute_content(
                            tag, 'hai3').split(',')
                    ],
                ]

                # DEL: we can't only initialize the main player, we must initialize
                # other players as well.
                #table.player.init_hand(hands[self.player_position])

                # ADD: initialize all players on the table
                table.players[0].init_hand(hands[self.player_position])
                table.players[1].init_hand(hands[(self.player_position + 1) %
                                                 4])
                table.players[2].init_hand(hands[(self.player_position + 2) %
                                                 4])
                table.players[3].init_hand(hands[(self.player_position + 3) %
                                                 4])

                # ADD: when restart a new game, we need to reinitialize the config
                self.extract_features.__init__()

            # We must deal with ALL players.
            #if player_draw_regex.match(tag) and 'UN' not in tag:
            if draw_regex.match(tag) and 'UN' not in tag:
                tile = self.decoder.parse_tile(tag)

                # CHG: we must deal with ALL players
                #table.player.draw_tile(tile)
                if "T" in tag:
                    table.players[0].draw_tile(tile)
                elif "U" in tag:
                    table.players[1].draw_tile(tile)
                elif "V" in tag:
                    table.players[2].draw_tile(tile)
                elif "W" in tag:
                    table.players[3].draw_tile(tile)
                    #print("After draw `W`:", table.players[3].tiles)

            if discard_regex.match(tag) and 'DORA' not in tag:
                tile = self.decoder.parse_tile(tag)
                player_sign = tag.upper()[1]

                # TODO: I don't know why the author wrote the code as below, the
                # player_seat won't work if we use self._normalize_position. This
                # might be a tricky part, and we need to review it later.
                #player_seat = self._normalize_position(self.player_position, discard_tags.index(player_sign))

                # Temporally solution to modify the player_seat
                player_seat = (discard_tags.index(player_sign) +
                               self.player_position) % 4
                #print("updated player seat:",player_seat)

                if player_seat == 0:
                    table.players[player_seat].discard_tile(
                        DiscardOption(table.players[player_seat], tile // 4, 0,
                                      [], 0))
                else:
                    # ADD: we must take care of ALL players
                    tile_to_discard = tile

                    is_tsumogiri = tile_to_discard == table.players[
                        player_seat].last_draw
                    # it is important to use table method,
                    # to recalculate revealed tiles and etc.
                    table.add_discarded_tile(player_seat, tile_to_discard,
                                             is_tsumogiri)

                    #print("seat:",player_seat)
                    #print("tiles:", TilesConverter.to_one_line_string(table.players[player_seat].tiles), " discard?:", TilesConverter.to_one_line_string([tile_to_discard]))
                    table.players[player_seat].tiles.remove(tile_to_discard)

                    # DEL
                    #table.add_discarded_tile(player_seat, tile, False)

            if '<N who=' in tag:
                meld = self.decoder.parse_meld(tag)
                #player_seat = self._normalize_position(self.player_position, meld.who)
                # Again, we change the player_seat here
                player_seat = (meld.who + self.player_position) % 4
                table.add_called_meld(player_seat, meld)

                #if player_seat == 0:
                # CHG: we need to handle ALL players here
                if True:
                    # we had to delete called tile from hand
                    # to have correct tiles count in the hand
                    if meld.type != Meld.KAN and meld.type != Meld.CHANKAN:
                        table.players[player_seat].draw_tile(meld.called_tile)

            if '<REACH' in tag and 'step="1"' in tag:
                who_called_riichi = self._normalize_position(
                    self.player_position,
                    self.decoder.parse_who_called_riichi(tag))
                table.add_called_riichi(who_called_riichi)

            # This part is to extract the features that will be used to train
            # our model.
            try:
                next_tag = self.round_content[n + 1]
            except IndexError:
                next_tag = ""
            if '<AGARI' in next_tag:
                who_regex = re.compile("who=\"\d+\"")
                fromWho_regex = re.compile("fromWho=\"\d+\"")
                sc_regex = "sc=\"[+-]?\d+,[+-]?\d+,[+-]?\d+,[+-]?\d+,[+-]?\d+,[+-]?\d+,[+-]?\d+,[+-]?\d+\""
                score_regex = re.compile(sc_regex)
                who = int(
                    who_regex.search(next_tag).group(0).replace(
                        '"', '').split("=")[1])
                fromWho = int(
                    fromWho_regex.search(next_tag).group(0).replace(
                        '"', '').split("=")[1])
                scores = [
                    float(s)
                    for s in score_regex.search(next_tag).group(0).replace(
                        '"', '').split("=")[1].split(",")
                ]
                score = scores[fromWho * 2 + 1]
                player_seat, features = self.execute_extraction(tag, table)

                #                # tsumo is not a valid sample for our training.
                #                if (who!=fromWho): # not tsumo (lose the score to winner, score<0)
                #                    if (features is not None) and (player_seat is not None) and (score<0):
                #                        # The first element before ";" is table_info, therefore player_info starts
                #                        # from index 1, and we put who+1 here.
                #                        self.feature_to_logger(features, who+1, score)
                #                        score = 1

                # tsumo is a valid sample for our training
                if (who == fromWho):  # tsumo (win the score, score>0)
                    if (features
                            is not None) and (player_seat
                                              is not None) and (score > 0):
                        self.feature_to_logger(features, who + 1, score)
                        score = -1

            else:
                player_seat, features = self.execute_extraction(tag, table)

        if not dry_run:
            tile = self.decoder.parse_tile(self.stop_tag)
            print('Hand: {}'.format(table.player.format_hand_for_print(tile)))

            # to rebuild all caches
            table.player.draw_tile(tile)
            tile = table.player.discard_tile()

            # real run, you can stop debugger here
            table.player.draw_tile(tile)
            tile = table.player.discard_tile()

            print('Discard: {}'.format(
                TilesConverter.to_one_line_string([tile])))

    def feature_to_logger(self, features, player_seat, score):
        features_list = features.split(";")
        assert len(features_list) == 6, "<D> Features format incorrect!"
        table_info = features_list[0]
        player_info = features_list[player_seat]
        logger2.info(table_info + ";" + player_info + ";" + str(score))

    def execute_extraction(self, tag, table):
        """
        D/E/F/G are for discards
        T/U/V/W are for draws
        """
        if ('<T' in tag) or ('<D' in tag):
            features = self.extract_features.get_scores_features(table)
            return 1, features
        if ('<U' in tag) or ('<E' in tag):
            features = self.extract_features.get_scores_features(table)
            return 2, features
        if ('<V' in tag) or ('<F' in tag):
            features = self.extract_features.get_scores_features(table)
            return 3, features
        if ('<W' in tag) or ('<G' in tag):
            features = self.extract_features.get_scores_features(table)
            return 4, features
        return None, None


#    def execute_extraction(self, tag, score, table, to_logger):
#        if '<D' in tag:
#            #features = self.extract_features.get_is_waiting_features(table)
#            #features = self.extract_features.get_waiting_tiles_features(table)
#            features = self.extract_features.get_scores_features(score, table)
#            if (features is not None) and to_logger:
#                features_list = features.split(";")
#                assert len(features_list)==6, "<D> Features format incorrect!"
#                score_info = features_list[0]
#                player_info = features_list[1]
#                logger2.info(score_info + ";" + player_info)
#
#        if '<E' in tag:
#            #features = self.extract_features.get_is_waiting_features(table)
#            #features = self.extract_features.get_waiting_tiles_features(table)
#            features = self.extract_features.get_scores_features(score, table)
#            if (features is not None) and to_logger:
#                features_list = features.split(";")
#                assert len(features_list)==6, "<E> Features format incorrect!"
#                score_info = features_list[0]
#                player_info = features_list[2]
#                logger2.info(score_info + ";" + player_info)
#
#        if '<F' in tag:
#            #features = self.extract_features.get_is_waiting_features(table)
#            #features = self.extract_features.get_waiting_tiles_features(table)
#            features = self.extract_features.get_scores_features(score, table)
#            if (features is not None) and to_logger:
#                features_list = features.split(";")
#                assert len(features_list)==6, "<F> Features format incorrect!"
#                score_info = features_list[0]
#                player_info = features_list[3]
#                logger2.info(score_info + ";" + player_info)
#
#        if '<G' in tag:
#            #features = self.extract_features.get_is_waiting_features(table)
#            #features = self.extract_features.get_waiting_tiles_features(table)
#            features = self.extract_features.get_scores_features(score, table)
#            if (features is not None) and to_logger:
#                features_list = features.split(";")
#                assert len(features_list)==6, "<G> Features format incorrect!"
#                score_info = features_list[0]
#                player_info = features_list[4]
#                logger2.info(score_info + ";" + player_info)

    def reproduce_all(self, dry_run=False):
        for r in self.rounds:
            self.round_content = r
            self.reproduce(dry_run=dry_run)
            print("--------------------------------------\n")

    def _normalize_position(self, who, from_who):
        positions = [0, 1, 2, 3]
        return positions[who - from_who]

    def _parse_url(self, log_url):
        temp = log_url.split('?')[1].split('&')
        log_id, player, round_number = '', 0, 0
        for item in temp:
            item = item.split('=')
            if 'log' == item[0]:
                log_id = item[1]
            if 'tw' == item[0]:
                player = int(item[1])
            if 'ts' == item[0]:
                round_number = int(item[1])
        return log_id, player, round_number

    def _download_log_content(self, log_id):
        """
        Check the log file, and if it is not there download it from tenhou.net
        :param log_id:
        :return:
        """
        temp_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                   'logs')
        if not os.path.exists(temp_folder):
            os.mkdir(temp_folder)

        log_file = os.path.join(temp_folder, log_id)
        if os.path.exists(log_file):
            with open(log_file, 'r') as f:
                return f.read()
        else:
            url = 'http://e.mjv.jp/0/log/?{0}'.format(log_id)
            response = requests.get(url)

            with open(log_file, 'w') as f:
                f.write(response.text)

            return response.text

    def _parse_rounds(self, log_content):
        """
        Build list of round tags
        :param log_content:
        :return:
        """
        rounds = []

        game_round = []
        tag_start = 0
        tag = None
        for x in range(0, len(log_content)):
            if log_content[x] == '>':
                tag = log_content[tag_start:x + 1]
                tag_start = x + 1

            # not useful tags
            if tag and ('mjloggm' in tag or 'TAIKYOKU' in tag):
                tag = None

            # new round was started
            if tag and 'INIT' in tag:
                rounds.append(game_round)
                game_round = []

            # the end of the game
            if tag and 'owari' in tag:
                rounds.append(game_round)

            if tag:
                # to save some memory we can remove not needed information from logs
                if 'INIT' in tag:
                    # we dont need seed information
                    find = re.compile(r'shuffle="[^"]*"')
                    tag = find.sub('', tag)

                # add processed tag to the round
                game_round.append(tag)
                tag = None

        return rounds[1:]
Beispiel #17
0
class TenhouLogReproducer(object):
    """
    The way to debug bot decisions that it made in real tenhou.net games
    """

    def __init__(self, mjlog_file=None, log_url=None, stop_tag=None):
        if log_url:
            log_id, player_position, needed_round = self._parse_url(log_url)
            log_content = self._download_log_content(log_id)
        elif mjlog_file:
            with open(mjlog_file, encoding="utf8") as f:
                log_id = mjlog_file.split("/")[-1].split(".")[0]
                player_position = 0 # tw: seat
                needed_round = 1 # ts: round
                log_content = f.read()
        rounds = self._parse_rounds(log_content)
        
        self.player_position = player_position
        self.round_content = rounds[needed_round]
        self.stop_tag = stop_tag
        self.decoder = TenhouDecoder()
        
        
        # ADD: to get results of all rounds
        self.rounds = rounds
        # ADD: to extract features to be saved
        self.extract_features = ExtractFeatures()

    def reproduce(self, dry_run=False):
        draw_tags = ['T', 'U', 'V', 'W']
        discard_tags = ['D', 'E', 'F', 'G']


        player_draw = draw_tags[self.player_position]
        player_draw_regex = re.compile('^<[{}]+\d*'.format(''.join(player_draw)))
        
        draw_regex = re.compile('^<[{}]+\d*'.format(''.join(draw_tags)))
        discard_regex = re.compile('^<[{}]+\d*'.format(''.join(discard_tags)))

        table = Table()
        score = 1
        
        skip = False # We neglect those unwanted records
        clean_records = [] # We use a list to store the clean records
        
        for n, tag in enumerate(self.round_content):
            if dry_run:
                if (draw_regex.match(tag) and 'UN' not in tag) or (discard_regex.match(tag) and 'DORA' not in tag):
                    tile = self.decoder.parse_tile(tag)
                    print("%s %s"%(tag, TilesConverter.to_one_line_string([tile])))
                else:
                    print(tag)

            if not dry_run and tag == self.stop_tag:
                break

            if 'INIT' in tag:
                values = self.decoder.parse_initial_values(tag)

                shifted_scores = []
                for x in range(0, 4):
                    shifted_scores.append(values['scores'][self._normalize_position(x, self.player_position)])

                table.init_round(
                    values['round_number'],
                    values['count_of_honba_sticks'],
                    values['count_of_riichi_sticks'],
                    values['dora_indicator'],
                    self._normalize_position(self.player_position, values['dealer']),
                    shifted_scores,
                )

                hands = [
                    [int(x) for x in self.decoder.get_attribute_content(tag, 'hai0').split(',')],
                    [int(x) for x in self.decoder.get_attribute_content(tag, 'hai1').split(',')],
                    [int(x) for x in self.decoder.get_attribute_content(tag, 'hai2').split(',')],
                    [int(x) for x in self.decoder.get_attribute_content(tag, 'hai3').split(',')],
                ]
                 
                # DEL: we can't only initialize the main player, we must initialize
                # other players as well.
                #table.player.init_hand(hands[self.player_position])
                
                # ADD: initialize all players on the table
                table.players[0].init_hand(hands[self.player_position])
                table.players[1].init_hand(hands[(self.player_position+1)%4])
                table.players[2].init_hand(hands[(self.player_position+2)%4])
                table.players[3].init_hand(hands[(self.player_position+3)%4])
                
                # ADD: when restart a new game, we need to reinitialize the config
                self.extract_features.__init__()
                # raw records of a new game
                raw_records = []

            # Trigger skip condition after <INIT>, b/c <INIT> has higher priority than skip
            if skip:
                continue

            # We must deal with ALL players.
            #if player_draw_regex.match(tag) and 'UN' not in tag:
            if draw_regex.match(tag) and 'UN' not in tag:
                tile = self.decoder.parse_tile(tag)
                
                # CHG: we must deal with ALL players
                #table.player.draw_tile(tile)
                if "T" in tag:
                    table.players[0].draw_tile(tile)
                elif "U" in tag:
                    table.players[1].draw_tile(tile)
                elif "V" in tag:
                    table.players[2].draw_tile(tile)
                elif "W" in tag:
                    table.players[3].draw_tile(tile)
                    #print("After draw `W`:", table.players[3].tiles)
                
            if discard_regex.match(tag) and 'DORA' not in tag:
                tile = self.decoder.parse_tile(tag)
                
                player_sign = tag.upper()[1]
                
                # TODO: I don't know why the author wrote the code as below, the 
                # player_seat won't work if we use self._normalize_position. This 
                # might be a tricky part, and we need to review it later.
                #player_seat = self._normalize_position(self.player_position, discard_tags.index(player_sign))
                
                # Temporally solution to modify the player_seat
                player_seat = (discard_tags.index(player_sign) + self.player_position)%4

                # Whenever a tile is discarded, the other players will check their
                # hands to see if they could call meld (steal the tile).
                try:
                    next_tag = self.round_content[n+1]
                except:
                    next_tag = ""
                for i in range(4):
                    if i!=player_seat:
                        is_kamicha_discard = (i-1==player_seat)
                        comb = table.players[i].get_possible_melds(tile, is_kamicha_discard)
                        table.players[i].possible_melds = comb   
                        if len(comb)>0: # he can call melds now
                              #print("\nplayer %s closed hand: %s"%(i,[t//4 for t in table.players[i].closed_hand]))
                              print("player %s closed hand: %s"%(i,TilesConverter.to_one_line_string(table.players[i].closed_hand)))
                              print("Tag: %s\nNext tag: %s"%(tag, next_tag))
                              if '<N who=' in next_tag:
                                  meld = self.decoder.parse_meld(next_tag)
                                  # TODO: Obviously the player seat is confusing again. I can't
                                  # get this part fixed. So let's just manually fix it for the moment.
                                  meld.from_who = player_seat
                                  print("%s meld from %s: get %s to form %s(%s): %s\n"%(meld.who, meld.from_who, meld.called_tile, meld.type, meld.opened, meld.tiles))
                                  assert meld.called_tile==tile, "Called tile NOT equal to discarded tile!"
                                  meld_tiles = [t//4 for t in meld.tiles]
                              else:
                                  meld_tiles = []
                              # This part is to extract the features for stealing
                              features = self.extract_features.get_stealing_features(table)
                              # write data to log file
                              # Note: since the first info is table info, player info starts
                              # from 1, so we use (i+1) here.
                              self.feature_to_logger(features, i+1, meld_tiles, tile, comb)
                
                if player_seat == 0:
                    table.players[player_seat].discard_tile(DiscardOption(table.players[player_seat], tile // 4, 0, [], 0))
                else:
                    # ADD: we must take care of ALL players
                    tile_to_discard = tile
            
                    is_tsumogiri = tile_to_discard == table.players[player_seat].last_draw
                    # it is important to use table method,
                    # to recalculate revealed tiles and etc.
                    table.add_discarded_tile(player_seat, tile_to_discard, is_tsumogiri)
                    
                    table.players[player_seat].tiles.remove(tile_to_discard)
            
                # This part is to extract the features we need.    
#                player_seat, features = self.execute_extraction(tag, table)
#                raw_records.append((player_seat, features))

            if '<N who=' in tag:
                meld = self.decoder.parse_meld(tag)
                #player_seat = self._normalize_position(self.player_position, meld.who)
                # Again, we change the player_seat here
                player_seat = (meld.who + self.player_position) % 4
                table.add_called_meld(player_seat, meld)

                #if player_seat == 0:
                # CHG: we need to handle ALL players here    
                if True:
                    # we had to delete called tile from hand
                    # to have correct tiles count in the hand
                    if meld.type != Meld.KAN and meld.type != Meld.CHANKAN:
                        table.players[player_seat].draw_tile(meld.called_tile)

            # [Joseph]: For old records, there is no 'step' in the tag. We need to take care of it
            if ('<REACH' in tag and 'step="1"' in tag) or ('<REACH' in tag and 'step' not in tag):
                
                # [Joseph] I don't know why he used _normalize_position here, from which I got incorrect
                # positions. Therefore I simply use the parsed result instead.
#                who_called_riichi = self._normalize_position(self.player_position,
#                                                             self.decoder.parse_who_called_riichi(tag))
                who_called_riichi = self.decoder.parse_who_called_riichi(tag)
                
                table.add_called_riichi(who_called_riichi)
                
#                print("\n")
#                print("{} Riichi!".format(who_called_riichi))
#                print("self.player_position: {}".format(self.player_position))
#                print("parse: {}".format(self.decoder.parse_who_called_riichi(tag)))
#                print("\n")
                # We need to delete those unnecessary records for one player mahjong
#                for (player_seat, features) in raw_records:
#                    if player_seat==(who_called_riichi+1):
#                        clean_records.append((player_seat,features))
                
                skip = True # skip all remaining tags untill <INIT>
        
        # Write the records to log        
#        for player_seat, features in clean_records:
#            self.feature_to_logger(features, player_seat)
              
            # This part is to extract the features that will be used to train
            # our model.
#            try:
#                next_tag = self.round_content[n+1]
#            except IndexError:
#                next_tag = ""
#            if '<AGARI' in next_tag:           
#                who_regex = re.compile("who=\"\d+\"")
#                fromWho_regex = re.compile("fromWho=\"\d+\"")           
#                sc_regex = "sc=\"[+-]?\d+,[+-]?\d+,[+-]?\d+,[+-]?\d+,[+-]?\d+,[+-]?\d+,[+-]?\d+,[+-]?\d+\""
#                score_regex = re.compile(sc_regex)
#                machi_regex = re.compile("machi=\"\d+\"")
#                
#                who = int(who_regex.search(next_tag).group(0).replace('"','').split("=")[1])
#                fromWho = int(fromWho_regex.search(next_tag).group(0).replace('"','').split("=")[1])
#                scores = [float(s) for s in score_regex.search(next_tag).group(0).replace('"','').split("=")[1].split(",")]              
#                machi = int(machi_regex.search(next_tag).group(0).replace('"','').split("=")[1])
#                score = scores[fromWho*2+1] 
#                player_seat, features = self.execute_extraction(tag, table)
#                
#                if (who!=fromWho): # tsumo is not a valid sample for our training.                    
#                    if (features is not None) and (player_seat is not None) and (score<0):
#                        # The first element before ";" is table_info, therefor player_info starts
#                        # from index 1, and we put who+1 here.
#                        self.feature_to_logger(features, who+1, machi//4, score)
#                        score = 1
#                    #print("\n{}\n{}\n".format(tag,table.players[who].tiles))
#            else:
#                player_seat, features = self.execute_extraction(tag, table)
         

                   
        if not dry_run:
            tile = self.decoder.parse_tile(self.stop_tag)
            print('Hand: {}'.format(table.player.format_hand_for_print(tile)))

            # to rebuild all caches
            table.player.draw_tile(tile)
            tile = table.player.discard_tile()

            # real run, you can stop debugger here
            table.player.draw_tile(tile)
            tile = table.player.discard_tile()

            print('Discard: {}'.format(TilesConverter.to_one_line_string([tile])))
            
    def feature_to_logger(self, features, player_seat, meld_tiles, tile, comb):
        """
        param features:
        """
        features_list = features.split(";")
        assert len(features_list)==6, "<D> Features format incorrect!"
        table_info = features_list[0]
        player_info = features_list[player_seat]
        
        d1, d2 = table_info, player_info
        
        (table_count_of_honba_sticks,
         table_count_of_remaining_tiles,
         table_count_of_riichi_sticks,
         table_round_number,
         table_round_wind,
         table_turns,
         table_dealer_seat,
         table_dora_indicators,
         table_dora_tiles,
         table_revealed_tiles) = ast.literal_eval(d1)
        
        (player_winning_tiles,                   
         player_discarded_tiles,
         player_closed_hand,
         player_dealer_seat,
         player_in_riichi,
         player_is_dealer,
         player_is_open_hand,  
         player_last_draw,            
         player_melds,                
         player_name,
         player_position,
         player_rank,
         player_scores,
         player_seat,
         player_uma) = ast.literal_eval(d2)
        
#        if player_discarded_tiles:
#            player_discard = player_discarded_tiles[-1]
#        else:
#            player_discard = -1
                
        #logger2.info(table_info + ";" + player_info + ";" + str(machi_34) + ";" + str(score))    
#        logger2.info(str(player_last_draw) + ";" + 
#                     str(player_discard) + ";" + 
#                     str(player_closed_hand) + ";" +
#                     str(table_revealed_tiles)
#                     )

        logger2.info(str(meld_tiles) + ";" + 
                     str(tile) + ";" +  # This info might not be used, but let's keep it for checking purpose 
                     str(comb) + ";" +  # This info might not be used, but let's just keep it for checking purpose
                     str(player_closed_hand) + ";" +
                     str(player_melds) + ";" +
                     str(table_revealed_tiles) + ";" + 
                     str(table_turns)
                     )    
        
    def execute_extraction(self, table):
        features = self.extract_features.get_stealing_features(table)             
        return features
     
    
#    def execute_extraction(self, tag, score, table, to_logger):
#        if '<D' in tag:
#            #features = self.extract_features.get_is_waiting_features(table)
#            #features = self.extract_features.get_waiting_tiles_features(table)
#            features = self.extract_features.get_scores_features(score, table)
#            if (features is not None) and to_logger:
#                features_list = features.split(";")
#                assert len(features_list)==6, "<D> Features format incorrect!"
#                score_info = features_list[0]
#                player_info = features_list[1]
#                logger2.info(score_info + ";" + player_info)
#                
#        if '<E' in tag:
#            #features = self.extract_features.get_is_waiting_features(table)
#            #features = self.extract_features.get_waiting_tiles_features(table)
#            features = self.extract_features.get_scores_features(score, table)
#            if (features is not None) and to_logger:
#                features_list = features.split(";")
#                assert len(features_list)==6, "<E> Features format incorrect!"
#                score_info = features_list[0]
#                player_info = features_list[2]
#                logger2.info(score_info + ";" + player_info)
#                
#        if '<F' in tag:
#            #features = self.extract_features.get_is_waiting_features(table)
#            #features = self.extract_features.get_waiting_tiles_features(table)
#            features = self.extract_features.get_scores_features(score, table)
#            if (features is not None) and to_logger:
#                features_list = features.split(";")
#                assert len(features_list)==6, "<F> Features format incorrect!"
#                score_info = features_list[0]
#                player_info = features_list[3]
#                logger2.info(score_info + ";" + player_info)
#               
#        if '<G' in tag:
#            #features = self.extract_features.get_is_waiting_features(table)
#            #features = self.extract_features.get_waiting_tiles_features(table)
#            features = self.extract_features.get_scores_features(score, table)
#            if (features is not None) and to_logger:
#                features_list = features.split(";")
#                assert len(features_list)==6, "<G> Features format incorrect!"
#                score_info = features_list[0]
#                player_info = features_list[4]
#                logger2.info(score_info + ";" + player_info)
                
       
            
    def reproduce_all(self, dry_run=False):
        for r in self.rounds:
            self.round_content = r
            self.reproduce(dry_run=dry_run)
            print("--------------------------------------\n")

    def _normalize_position(self, who, from_who):
        positions = [0, 1, 2, 3]
        return positions[who - from_who]

    def _parse_url(self, log_url):
        temp = log_url.split('?')[1].split('&')
        log_id, player, round_number = '', 0, 0
        for item in temp:
            item = item.split('=')
            if 'log' == item[0]:
                log_id = item[1]
            if 'tw' == item[0]:
                player = int(item[1])
            if 'ts' == item[0]:
                round_number = int(item[1])
        return log_id, player, round_number

    def _download_log_content(self, log_id):
        """
        Check the log file, and if it is not there download it from tenhou.net
        :param log_id:
        :return:
        """
        temp_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'logs')
        if not os.path.exists(temp_folder):
            os.mkdir(temp_folder)

        log_file = os.path.join(temp_folder, log_id)
        if os.path.exists(log_file):
            with open(log_file, 'r') as f:
                return f.read()
        else:
            url = 'http://e.mjv.jp/0/log/?{0}'.format(log_id)
            response = requests.get(url)

            with open(log_file, 'w') as f:
                f.write(response.text)

            return response.text

    def _parse_rounds(self, log_content):
        """
        Build list of round tags
        :param log_content:
        :return:
        """
        rounds = []

        game_round = []
        tag_start = 0
        tag = None
        for x in range(0, len(log_content)):
            if log_content[x] == '>':
                tag = log_content[tag_start:x + 1]
                tag_start = x + 1

            # not useful tags
            if tag and ('mjloggm' in tag or 'TAIKYOKU' in tag):
                tag = None

            # new round was started
            if tag and 'INIT' in tag:
                rounds.append(game_round)
                game_round = []

            # the end of the game
            if tag and 'owari' in tag:
                rounds.append(game_round)

            if tag:
                # to save some memory we can remove not needed information from logs
                if 'INIT' in tag:
                    # we dont need seed information
                    find = re.compile(r'shuffle="[^"]*"')
                    tag = find.sub('', tag)

                # add processed tag to the round
                game_round.append(tag)
                tag = None

        return rounds[1:]
class TenhouLogReproducer(object):
    """
    The way to debug bot decisions that it made in real tenhou.net games
    """

    def __init__(self, log_url, stop_tag=None):
        log_id, player_position, needed_round = self._parse_url(log_url)
        log_content = self._download_log_content(log_id)
        rounds = self._parse_rounds(log_content)

        self.player_position = player_position
        self.round_content = rounds[needed_round]
        self.stop_tag = stop_tag
        self.decoder = TenhouDecoder()

    def reproduce(self, dry_run=False):
        draw_tags = ['T', 'U', 'V', 'W']
        discard_tags = ['D', 'E', 'F', 'G']

        player_draw = draw_tags[self.player_position]

        player_draw_regex = re.compile('^<[{}]+\d*'.format(''.join(player_draw)))
        discard_regex = re.compile('^<[{}]+\d*'.format(''.join(discard_tags)))

        table = Table()
        for tag in self.round_content:
            if player_draw_regex.match(tag) and 'UN' not in tag:
                print('Player draw')
                tile = self.decoder.parse_tile(tag)
                table.player.draw_tile(tile)

            if dry_run:
                if self._is_draw(tag):
                    print('<-', TilesConverter.to_one_line_string([self._parse_tile(tag)]), tag)
                elif self._is_discard(tag):
                    print('->', TilesConverter.to_one_line_string([self._parse_tile(tag)]), tag)
                elif self._is_init_tag(tag):
                    hands = {
                        0: [int(x) for x in self._get_attribute_content(tag, 'hai0').split(',')],
                        1: [int(x) for x in self._get_attribute_content(tag, 'hai1').split(',')],
                        2: [int(x) for x in self._get_attribute_content(tag, 'hai2').split(',')],
                        3: [int(x) for x in self._get_attribute_content(tag, 'hai3').split(',')],
                    }
                    print('Initial hand:', TilesConverter.to_one_line_string(hands[self.player_position]))
                else:
                    print(tag)

            if not dry_run and tag == self.stop_tag:
                break

            if 'INIT' in tag:
                values = self.decoder.parse_initial_values(tag)

                shifted_scores = []
                for x in range(0, 4):
                    shifted_scores.append(values['scores'][self._normalize_position(x, self.player_position)])

                table.init_round(
                    values['round_wind_number'],
                    values['count_of_honba_sticks'],
                    values['count_of_riichi_sticks'],
                    values['dora_indicator'],
                    self._normalize_position(self.player_position, values['dealer']),
                    shifted_scores,
                )

                hands = [
                    [int(x) for x in self.decoder.get_attribute_content(tag, 'hai0').split(',')],
                    [int(x) for x in self.decoder.get_attribute_content(tag, 'hai1').split(',')],
                    [int(x) for x in self.decoder.get_attribute_content(tag, 'hai2').split(',')],
                    [int(x) for x in self.decoder.get_attribute_content(tag, 'hai3').split(',')],
                ]

                table.player.init_hand(hands[self.player_position])

            if discard_regex.match(tag) and 'DORA' not in tag:
                tile = self.decoder.parse_tile(tag)
                player_sign = tag.upper()[1]
                player_seat = self._normalize_position(self.player_position, discard_tags.index(player_sign))

                if player_seat == 0:
                    table.player.discard_tile(tile)
                else:
                    table.add_discarded_tile(player_seat, tile, False)

            if '<N who=' in tag:
                meld = self.decoder.parse_meld(tag)
                player_seat = self._normalize_position(self.player_position, meld.who)
                table.add_called_meld(player_seat, meld)

                if player_seat == 0:
                    # we had to delete called tile from hand
                    # to have correct tiles count in the hand
                    if meld.type != Meld.KAN and meld.type != Meld.CHANKAN:
                        table.player.draw_tile(meld.called_tile)

            if '<REACH' in tag and 'step="1"' in tag:
                who_called_riichi = self._normalize_position(self.player_position,
                                                             self.decoder.parse_who_called_riichi(tag))
                table.add_called_riichi(who_called_riichi)

        if not dry_run:
            tile = self.decoder.parse_tile(self.stop_tag)
            print('Hand: {}'.format(table.player.format_hand_for_print(tile)))

            # to rebuild all caches
            table.player.draw_tile(tile)
            tile = table.player.discard_tile()

            # real run, you can stop debugger here
            table.player.draw_tile(tile)
            tile = table.player.discard_tile()

            print('Discard: {}'.format(TilesConverter.to_one_line_string([tile])))

    def _normalize_position(self, who, from_who):
        positions = [0, 1, 2, 3]
        return positions[who - from_who]

    def _parse_url(self, log_url):
        temp = log_url.split('?')[1].split('&')
        log_id, player, round_wind = '', 0, 0
        for item in temp:
            item = item.split('=')
            if 'log' == item[0]:
                log_id = item[1]
            if 'tw' == item[0]:
                player = int(item[1])
            if 'ts' == item[0]:
                round_wind = int(item[1])
        return log_id, player, round_wind

    def _download_log_content(self, log_id):
        """
        Check the log file, and if it is not there download it from tenhou.net
        :param log_id:
        :return:
        """
        temp_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'logs')
        if not os.path.exists(temp_folder):
            os.mkdir(temp_folder)

        log_file = os.path.join(temp_folder, log_id)
        if os.path.exists(log_file):
            with open(log_file, 'r') as f:
                return f.read()
        else:
            url = 'http://e.mjv.jp/0/log/?{0}'.format(log_id)
            response = requests.get(url)

            with open(log_file, 'w') as f:
                f.write(response.text)

            return response.text

    def _parse_rounds(self, log_content):
        """
        Build list of round tags
        :param log_content:
        :return:
        """
        rounds = []

        game_round = []
        tag_start = 0
        tag = None
        for x in range(0, len(log_content)):
            if log_content[x] == '>':
                tag = log_content[tag_start:x + 1]
                tag_start = x + 1

            # not useful tags
            if tag and ('mjloggm' in tag or 'TAIKYOKU' in tag):
                tag = None

            # new round was started
            if tag and 'INIT' in tag:
                rounds.append(game_round)
                game_round = []

            # the end of the game
            if tag and 'owari' in tag:
                rounds.append(game_round)

            if tag:
                # to save some memory we can remove not needed information from logs
                if 'INIT' in tag:
                    # we dont need seed information
                    find = re.compile(r'shuffle="[^"]*"')
                    tag = find.sub('', tag)

                # add processed tag to the round
                game_round.append(tag)
                tag = None

        return rounds[1:]

    def _is_discard(self, tag):
        skip_tags = ['<GO', '<FURITEN', '<DORA']
        if any([x in tag for x in skip_tags]):
            return False

        match_discard = re.match(r"^<[defgDEFG]+\d*", tag)
        if match_discard:
            return True

        return False

    def _is_draw(self, tag):
        match_discard = re.match(r"^<[tuvwTUVW]+\d*", tag)
        if match_discard:
            return True

        return False

    def _parse_tile(self, tag):
        result = re.match(r'^<[defgtuvwDEFGTUVW]+\d*', tag).group()
        return int(result[2:])

    def _is_init_tag(self, tag):
        return tag and 'INIT' in tag

    def _get_attribute_content(self, tag, attribute_name):
        result = re.findall(r'{}="([^"]*)"'.format(attribute_name), tag)
        return result and result[0] or None
class TenhouLogReproducer(object):
    """
    The way to debug bot decisions that it made in real tenhou.net games
    """
    def __init__(self, log_url, stop_tag=None):
        log_id, player_position, needed_round = self._parse_url(log_url)
        log_content = self._download_log_content(log_id)
        rounds = self._parse_rounds(log_content)

        self.player_position = player_position
        self.round_content = rounds[needed_round]
        self.stop_tag = stop_tag
        self.decoder = TenhouDecoder()

    def reproduce(self, dry_run=False):
        draw_tags = ['T', 'U', 'V', 'W']
        discard_tags = ['D', 'E', 'F', 'G']

        player_draw = draw_tags[self.player_position]

        player_draw_regex = re.compile('^<[{}]+\d*'.format(
            ''.join(player_draw)))
        discard_regex = re.compile('^<[{}]+\d*'.format(''.join(discard_tags)))

        table = Table()
        for tag in self.round_content:
            if dry_run:
                print(tag)

            if not dry_run and tag == self.stop_tag:
                break

            if 'INIT' in tag:
                values = self.decoder.parse_initial_values(tag)

                shifted_scores = []
                for x in range(0, 4):
                    shifted_scores.append(
                        values['scores'][self._normalize_position(
                            x, self.player_position)])

                table.init_round(
                    values['round_number'],
                    values['count_of_honba_sticks'],
                    values['count_of_riichi_sticks'],
                    values['dora_indicator'],
                    self._normalize_position(self.player_position,
                                             values['dealer']),
                    shifted_scores,
                )

                hands = [
                    [
                        int(x) for x in self.decoder.get_attribute_content(
                            tag, 'hai0').split(',')
                    ],
                    [
                        int(x) for x in self.decoder.get_attribute_content(
                            tag, 'hai1').split(',')
                    ],
                    [
                        int(x) for x in self.decoder.get_attribute_content(
                            tag, 'hai2').split(',')
                    ],
                    [
                        int(x) for x in self.decoder.get_attribute_content(
                            tag, 'hai3').split(',')
                    ],
                ]

                table.player.init_hand(hands[self.player_position])

            if player_draw_regex.match(tag) and 'UN' not in tag:
                tile = self.decoder.parse_tile(tag)
                table.player.draw_tile(tile)

            if discard_regex.match(tag) and 'DORA' not in tag:
                tile = self.decoder.parse_tile(tag)
                player_sign = tag.upper()[1]
                player_seat = self._normalize_position(
                    self.player_position, discard_tags.index(player_sign))

                if player_seat == 0:
                    table.player.discard_tile(
                        DiscardOption(table.player, tile // 4, 0, [], 0))
                else:
                    table.add_discarded_tile(player_seat, tile, False)

            if '<N who=' in tag:
                meld = self.decoder.parse_meld(tag)
                player_seat = self._normalize_position(self.player_position,
                                                       meld.who)
                table.add_called_meld(player_seat, meld)

                if player_seat == 0:
                    # we had to delete called tile from hand
                    # to have correct tiles count in the hand
                    if meld.type != Meld.KAN and meld.type != Meld.CHANKAN:
                        table.player.draw_tile(meld.called_tile)

            if '<REACH' in tag and 'step="1"' in tag:
                who_called_riichi = self._normalize_position(
                    self.player_position,
                    self.decoder.parse_who_called_riichi(tag))
                table.add_called_riichi(who_called_riichi)

        if not dry_run:
            tile = self.decoder.parse_tile(self.stop_tag)
            print('Hand: {}'.format(table.player.format_hand_for_print(tile)))

            # to rebuild all caches
            table.player.draw_tile(tile)
            tile = table.player.discard_tile()

            # real run, you can stop debugger here
            table.player.draw_tile(tile)
            tile = table.player.discard_tile()

            print('Discard: {}'.format(
                TilesConverter.to_one_line_string([tile])))

    def _normalize_position(self, who, from_who):
        positions = [0, 1, 2, 3]
        return positions[who - from_who]

    def _parse_url(self, log_url):
        temp = log_url.split('?')[1].split('&')
        log_id, player, round_number = '', 0, 0
        for item in temp:
            item = item.split('=')
            if 'log' == item[0]:
                log_id = item[1]
            if 'tw' == item[0]:
                player = int(item[1])
            if 'ts' == item[0]:
                round_number = int(item[1])
        return log_id, player, round_number

    def _download_log_content(self, log_id):
        """
        Check the log file, and if it is not there download it from tenhou.net
        :param log_id:
        :return:
        """
        temp_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                   'logs')
        if not os.path.exists(temp_folder):
            os.mkdir(temp_folder)

        log_file = os.path.join(temp_folder, log_id)
        if os.path.exists(log_file):
            with open(log_file, 'r') as f:
                return f.read()
        else:
            url = 'http://e.mjv.jp/0/log/?{0}'.format(log_id)
            response = requests.get(url)

            with open(log_file, 'w') as f:
                f.write(response.text)

            return response.text

    def _parse_rounds(self, log_content):
        """
        Build list of round tags
        :param log_content:
        :return:
        """
        rounds = []

        game_round = []
        tag_start = 0
        tag = None
        for x in range(0, len(log_content)):
            if log_content[x] == '>':
                tag = log_content[tag_start:x + 1]
                tag_start = x + 1

            # not useful tags
            if tag and ('mjloggm' in tag or 'TAIKYOKU' in tag):
                tag = None

            # new round was started
            if tag and 'INIT' in tag:
                rounds.append(game_round)
                game_round = []

            # the end of the game
            if tag and 'owari' in tag:
                rounds.append(game_round)

            if tag:
                # to save some memory we can remove not needed information from logs
                if 'INIT' in tag:
                    # we dont need seed information
                    find = re.compile(r'shuffle="[^"]*"')
                    tag = find.sub('', tag)

                # add processed tag to the round
                game_round.append(tag)
                tag = None

        return rounds[1:]
    def parse_log(self, log_data, log_id):
        decoder = TenhouDecoder()
        finished_hand = FinishedHand()

        soup = BeautifulSoup(log_data, 'html.parser')
        elements = soup.find_all()

        settings.FIVE_REDS = True
        settings.OPEN_TANYAO = True

        total_hand = 0
        successful_hand = 0
        played_rounds = 0

        dealer = 0
        round_wind = EAST

        for tag in elements:
            if tag.name == 'go':
                game_rule_temp = int(tag.attrs['type'])

                # let's skip hirosima games
                hirosima = [177, 185, 241, 249]
                if game_rule_temp in hirosima:
                    print('0,0')
                    return

                # one round games
                skip_games = [2113]
                if game_rule_temp in skip_games:
                    print('0,0')
                    return

                no_red_five = [163, 167, 171, 175]
                if game_rule_temp in no_red_five:
                    settings.FIVE_REDS = False

                no_open_tanyao = [167, 175]
                if game_rule_temp in no_open_tanyao:
                    settings.OPEN_TANYAO = False

            if tag.name == 'taikyoku':
                dealer = int(tag.attrs['oya'])

            if tag.name == 'init':
                dealer = int(tag.attrs['oya'])
                seed = [int(i) for i in tag.attrs['seed'].split(',')]
                round_number = seed[0]

                if round_number < 4:
                    round_wind = EAST
                elif 4 <= round_number < 8:
                    round_wind = SOUTH
                elif 8 <= round_number < 12:
                    round_wind = WEST
                else:
                    round_wind = NORTH

                played_rounds += 1

            if tag.name == 'agari':
                success = True
                winner = int(tag.attrs['who'])
                from_who = int(tag.attrs['fromwho'])

                closed_hand = [int(i) for i in tag.attrs['hai'].split(',')]
                ten = [int(i) for i in tag.attrs['ten'].split(',')]
                dora_indicators = [
                    int(i) for i in tag.attrs['dorahai'].split(',')
                ]
                if 'dorahaiura' in tag.attrs:
                    dora_indicators += [
                        int(i) for i in tag.attrs['dorahaiura'].split(',')
                    ]

                yaku_list = []
                yakuman_list = []
                if 'yaku' in tag.attrs:
                    yaku_temp = [int(i) for i in tag.attrs['yaku'].split(',')]
                    yaku_list = yaku_temp[::2]
                    han = sum(yaku_temp[1::2])
                else:
                    yakuman_list = [
                        int(i) for i in tag.attrs['yakuman'].split(',')
                    ]
                    han = len(yakuman_list) * 13

                fu = ten[0]
                cost = ten[1]

                melds = []
                called_kan_indices = []
                if 'm' in tag.attrs:
                    for x in tag.attrs['m'].split(','):
                        #message = '<N who={} m={}>'.format(tag.attrs['who'], x)
                        # Modified[joseph]: added quotes to the params
                        message = '<N who="{}" m="{}">'.format(
                            tag.attrs['who'], x)
                        meld = decoder.parse_meld(message)
                        tiles = meld.tiles
                        if len(tiles) == 4:
                            called_kan_indices.append(tiles[0])
                            tiles = tiles[1:4]

                        # closed kan
                        if meld.from_who == 0:
                            closed_hand.extend(tiles)
                        else:
                            melds.append(tiles)

                # Modified[joseph]: We need to turn tile in 136 format to 34 format here
                melds34 = []
                for mld in melds:
                    mld34 = [tile_136_to_34(m) for m in mld]
                    melds34.append(mld34)

                hand = closed_hand

                if melds:
                    hand += reduce(lambda z, y: z + y, melds)

                win_tile = int(tag.attrs['machi'])

                is_tsumo = winner == from_who
                is_riichi = 1 in yaku_list
                is_ippatsu = 2 in yaku_list
                is_chankan = 3 in yaku_list
                is_rinshan = 4 in yaku_list
                is_haitei = 5 in yaku_list
                is_houtei = 6 in yaku_list
                is_daburu_riichi = 21 in yaku_list
                is_dealer = winner == dealer
                is_renhou = 36 in yakuman_list
                is_tenhou = 37 in yakuman_list
                is_chiihou = 38 in yakuman_list

                dif = winner - dealer
                winds = [EAST, SOUTH, WEST, NORTH]
                player_wind = winds[dif]

                result = finished_hand.estimate_hand_value(
                    hand,
                    win_tile,
                    is_tsumo=is_tsumo,
                    is_riichi=is_riichi,
                    is_dealer=is_dealer,
                    is_ippatsu=is_ippatsu,
                    is_rinshan=is_rinshan,
                    is_chankan=is_chankan,
                    is_haitei=is_haitei,
                    is_houtei=is_houtei,
                    is_daburu_riichi=is_daburu_riichi,
                    is_tenhou=is_tenhou,
                    is_renhou=is_renhou,
                    is_chiihou=is_chiihou,
                    round_wind=round_wind,
                    player_wind=player_wind,
                    called_kan_indices=called_kan_indices,
                    open_sets=melds34,  # melds, we use tile in 34 format
                    dora_indicators=dora_indicators)

                if result['error']:
                    logger.error('Error with hand calculation: {}'.format(
                        result['error']))
                    calculated_cost = 0
                    success = False
                else:
                    calculated_cost = result['cost'][
                        'main'] + result['cost']['additional'] * 2

                if success:
                    if result['fu'] != fu:
                        logger.error('Wrong fu: {} != {}'.format(
                            result['fu'], fu))
                        success = False

                    if result['han'] != han:
                        logger.error('Wrong han: {} != {}'.format(
                            result['han'], han))
                        success = False

                    if cost != calculated_cost:
                        logger.error('Wrong cost: {} != {}'.format(
                            cost, calculated_cost))
                        success = False

                if not success:
                    logger.error('http://e.mjv.jp/0/log/?{}'.format(log_id))
                    logger.error(
                        'http://tenhou.net/0/?log={}&tw={}&ts={}'.format(
                            log_id, winner, played_rounds - 1))
                    logger.error('Winner: {}, Dealer: {}'.format(
                        winner, dealer))
                    logger.error('Hand: {}'.format(
                        TilesConverter.to_one_line_string(hand)))
                    logger.error('Win tile: {}'.format(
                        TilesConverter.to_one_line_string([win_tile])))
                    logger.error('Open sets: {}'.format(melds))
                    logger.error('Called kans: {}'.format(
                        TilesConverter.to_one_line_string(called_kan_indices)))
                    logger.error('Our results: {}'.format(result))
                    logger.error('Tenhou results: {}'.format(tag.attrs))
                    logger.error('Dora: {}'.format(
                        TilesConverter.to_one_line_string(dora_indicators)))
                    logger.error('')
                else:
                    successful_hand += 1
                    print('Our results: {}'.format(result))
                    print("\t* dealer:{}, winner:{}, is_dealer:{}".format(
                        dealer, winner, is_dealer))
                    print("\t* Calculated cost: {}".format(calculated_cost))
                    print('Tenhou results: {}'.format(tag.attrs))
                    print("-----------------------------------\n")

                total_hand += 1

        print('{},{}'.format(successful_hand, total_hand))
Beispiel #21
0
class TenhouLogReproducer:
    """
    The way to debug bot decisions that it made in real tenhou.net games
    """
    def __init__(self, log_id, file_path, logger):
        self.decoder = TenhouDecoder()
        self.logger = logger

        if log_id:
            log_content = self._download_log_content(log_id)
        elif file_path:
            with open(file_path, "r") as f:
                log_content = f.read()
        else:
            raise AssertionError("log id or file path should be specified")

        self.rounds = self._parse_rounds(log_content)

    def print_meta_info(self):
        meta_information = {"players": [], "game_rounds": []}
        for round_item in self.rounds:
            for tag in round_item:
                if "<UN" in tag:
                    players = self.decoder.parse_names_and_ranks(tag)
                    if players:
                        meta_information[
                            "players"] = self.decoder.parse_names_and_ranks(
                                tag)

                if "INIT" in tag:
                    init_values = self.decoder.parse_initial_values(tag)
                    meta_information["game_rounds"].append({
                        "wind":
                        init_values["round_wind_number"] + 1,
                        "honba":
                        init_values["count_of_honba_sticks"],
                        "round_start_scores":
                        init_values["scores"],
                    })

        return meta_information

    def reproduce(self, player, wind, honba, needed_tile, action,
                  tile_number_to_stop):
        player_position = self._find_player_position(player)
        round_content = self._find_needed_round(wind, honba)

        draw_tags = ["T", "U", "V", "W"]
        discard_tags = ["D", "E", "F", "G"]

        player_draw = draw_tags[player_position]

        player_draw_regex = re.compile(r"^<[{}]+\d*".format(
            "".join(player_draw)))
        discard_regex = re.compile(r"^<[{}]+\d*".format("".join(discard_tags)))

        draw_regex = re.compile(r"^<[{}]+\d*".format("".join(draw_tags)))
        last_draws = {0: None, 1: None, 2: None, 3: None}

        table = Table()
        # TODO get this info from log content
        table.has_aka_dora = True
        table.has_open_tanyao = True
        table.player.init_logger(self.logger)

        players = {}
        for round_item in self.rounds:
            for tag in round_item:
                if "<UN" in tag:
                    players_temp = self.decoder.parse_names_and_ranks(tag)
                    if players_temp:
                        for x in players_temp:
                            players[x["seat"]] = x

        draw_tile_seen_number = 0
        enemy_discard_seen_number = 0
        for tag in round_content:
            if player_draw_regex.match(tag) and "UN" not in tag:
                tile = self.decoder.parse_tile(tag)
                table.count_of_remaining_tiles -= 1

                # is it time to stop reproducing?
                found_tile = TilesConverter.to_one_line_string(
                    [tile]) == needed_tile
                if action == "draw" and found_tile:
                    draw_tile_seen_number += 1
                    if draw_tile_seen_number == tile_number_to_stop:
                        self.logger.info("Stop on player draw")

                        discard_result = None
                        with_riichi = False
                        table.player.draw_tile(tile)

                        table.player.should_call_kan(
                            tile,
                            open_kan=False,
                            from_riichi=table.player.in_riichi)

                        if not table.player.in_riichi:
                            discard_result = table.player.discard_tile()
                            with_riichi = table.player.can_call_riichi()

                        return discard_result, with_riichi

                table.player.draw_tile(tile)

            if "INIT" in tag:
                values = self.decoder.parse_initial_values(tag)

                shifted_scores = []
                for x in range(0, 4):
                    shifted_scores.append(
                        values["scores"][self._normalize_position(
                            player_position, x)])

                table.init_round(
                    values["round_wind_number"],
                    values["count_of_honba_sticks"],
                    values["count_of_riichi_sticks"],
                    values["dora_indicator"],
                    self._normalize_position(values["dealer"],
                                             player_position),
                    shifted_scores,
                )

                hands = [
                    [
                        int(x) for x in self.decoder.get_attribute_content(
                            tag, "hai0").split(",")
                    ],
                    [
                        int(x) for x in self.decoder.get_attribute_content(
                            tag, "hai1").split(",")
                    ],
                    [
                        int(x) for x in self.decoder.get_attribute_content(
                            tag, "hai2").split(",")
                    ],
                    [
                        int(x) for x in self.decoder.get_attribute_content(
                            tag, "hai3").split(",")
                    ],
                ]

                table.player.init_hand(hands[player_position])
                table.player.name = players[player_position]["name"]

                self.logger.info("Init round info")
                self.logger.info(table.player.name)
                self.logger.info(f"Scores: {table.player.scores}")
                self.logger.info(
                    f"Wind: {DISPLAY_WINDS[table.player.player_wind]}")

            if "DORA hai" in tag:
                table.add_dora_indicator(
                    int(self._get_attribute_content(tag, "hai")))

            if draw_regex.match(tag) and "UN" not in tag:
                tile = self.decoder.parse_tile(tag)
                player_sign = tag.upper()[1]
                player_seat = self._normalize_position(
                    draw_tags.index(player_sign), player_position)
                last_draws[player_seat] = tile

            if discard_regex.match(tag) and "DORA" not in tag:
                tile = self.decoder.parse_tile(tag)
                player_sign = tag.upper()[1]
                player_seat = self._normalize_position(
                    discard_tags.index(player_sign), player_position)

                if player_seat == 0:
                    table.player.discard_tile(tile, force_tsumogiri=True)
                else:
                    # is it time to stop?
                    found_tile = TilesConverter.to_one_line_string(
                        [tile]) == needed_tile
                    is_kamicha_discard = player_seat == 3

                    if action == "enemy_discard" and found_tile:
                        enemy_discard_seen_number += 1
                        if enemy_discard_seen_number == tile_number_to_stop:
                            self.logger.info("Stop on enemy discard")
                            self._rebuild_bot_shanten_cache(table.player)
                            table.player.should_call_kan(tile,
                                                         open_kan=True,
                                                         from_riichi=False)
                            return table.player.try_to_call_meld(
                                tile, is_kamicha_discard)

                    is_tsumogiri = last_draws[player_seat] == tile
                    table.add_discarded_tile(player_seat,
                                             tile,
                                             is_tsumogiri=is_tsumogiri)

            if "<N who=" in tag:
                meld = self.decoder.parse_meld(tag)
                player_seat = self._normalize_position(meld.who,
                                                       player_position)
                table.add_called_meld(player_seat, meld)

                if player_seat == 0:
                    if meld.type != MeldPrint.KAN and meld.type != MeldPrint.SHOUMINKAN:
                        table.player.draw_tile(meld.called_tile)

            if "<REACH" in tag and 'step="1"' in tag:
                who_called_riichi = self._normalize_position(
                    self.decoder.parse_who_called_riichi(tag), player_position)
                table.add_called_riichi_step_one(who_called_riichi)

            if "<REACH" in tag and 'step="2"' in tag:
                who_called_riichi = self._normalize_position(
                    self.decoder.parse_who_called_riichi(tag), player_position)
                table.add_called_riichi_step_two(who_called_riichi)

    def _find_needed_round(self, wind, honba):
        found_round_item = None
        for round_item in self.rounds:
            for tag in round_item:
                if "INIT" in tag:
                    init_values = self.decoder.parse_initial_values(tag)
                    if init_values[
                            "round_wind_number"] + 1 == wind and init_values[
                                "count_of_honba_sticks"] == honba:
                        found_round_item = round_item
        if not found_round_item:
            raise Exception(
                f"Can't find wind={wind}, honba={honba} game round. "
                f"Check log with --meta tag first to be sure that these attrs are correct."
            )
        return found_round_item

    def _rebuild_bot_shanten_cache(self, player):
        isolated_tile_34 = find_isolated_tile_indices(
            TilesConverter.to_34_array(player.closed_hand))[0]
        isolated_tile_136 = isolated_tile_34 * 4
        player.table.revealed_tiles[isolated_tile_34] -= 1
        player.draw_tile(isolated_tile_136)
        player.discard_tile()

    def _find_player_position(self, player):
        # seat number was provided
        try:
            position = int(player)
            if position > 3:
                raise Exception("Player seat can't be more than 3")
            return position
        except ValueError:
            pass

        # player nickname was provided
        for round_item in self.rounds:
            for tag in round_item:
                if "<UN" in tag:
                    values = self.decoder.parse_names_and_ranks(tag)
                    found_player = [x for x in values if x["name"] == player]
                    if len(found_player) == 0 or len(found_player) > 1:
                        raise Exception(
                            f"Found players with '{player}' nickname: {len(found_player)}"
                        )
                    return found_player[0]["seat"]

    def _normalize_position(self, who, from_who):
        positions = [0, 1, 2, 3]
        return positions[who - from_who]

    def _download_log_content(self, log_id):
        """
        Check the log file, and if it is not there download it from tenhou.net
        """
        temp_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                   "logs")
        if not os.path.exists(temp_folder):
            os.mkdir(temp_folder)

        log_file = os.path.join(temp_folder, log_id)
        if os.path.exists(log_file):
            with open(log_file, "r") as f:
                return f.read()
        else:
            url = f"http://tenhou.net/0/log/?{log_id}"
            response = requests.get(url)

            with open(log_file, "w") as f:
                f.write(response.text)

            return response.text

    def _parse_rounds(self, log_content):
        """
        Parse xml log to lists of tags
        """
        rounds = []

        game_round = []
        tag_start = 0
        tag = None
        for x in range(0, len(log_content)):
            if log_content[x] == ">":
                tag = log_content[tag_start:x + 1]
                tag_start = x + 1

            # not useful tags
            if tag and ("mjloggm" in tag or "TAIKYOKU" in tag):
                tag = None

            # new round was started
            if tag and "INIT" in tag:
                rounds.append(game_round)
                game_round = []

            # the end of the game
            if tag and "owari" in tag:
                rounds.append(game_round)

            if tag:
                # to save some memory we can remove not needed information from logs
                if "INIT" in tag:
                    # we dont need seed information
                    find = re.compile(r'shuffle="[^"]*"')
                    tag = find.sub("", tag)

                # add processed tag to the round
                game_round.append(tag)
                tag = None

        return rounds

    def _is_discard(self, tag):
        skip_tags = ["<GO", "<FURITEN", "<DORA"]
        if any([x in tag for x in skip_tags]):
            return False

        match_discard = re.match(r"^<[defgDEFG]+\d*", tag)
        if match_discard:
            return True

        return False

    def _is_draw(self, tag):
        match_discard = re.match(r"^<[tuvwTUVW]+\d*", tag)
        if match_discard:
            return True

        return False

    def _parse_tile(self, tag):
        result = re.match(r"^<[defgtuvwDEFGTUVW]+\d*", tag).group()
        return int(result[2:])

    def _is_init_tag(self, tag):
        return tag and "INIT" in tag

    def _get_attribute_content(self, tag, attribute_name):
        result = re.findall(r'{}="([^"]*)"'.format(attribute_name), tag)
        return result and result[0] or None