Exemple #1
0
    def test_is_suukantsu(self):
        hand = FinishedHand()

        tiles = self._string_to_34_array(sou='111333', man='222', pin='44555')
        called_kan_indices = [
            self._string_to_34_tile(sou='1'),
            self._string_to_34_tile(sou='3'),
            self._string_to_34_tile(pin='5'),
            self._string_to_34_tile(man='2')
        ]
        self.assertTrue(
            hand.is_suukantsu(self._hand(tiles, 0), called_kan_indices))

        tiles = self._string_to_136_array(sou='111333', man='222', pin='44555')
        win_tile = self._string_to_136_tile(pin='4')
        open_sets = [
            self._string_to_open_34_set(sou='111'),
            self._string_to_open_34_set(sou='333')
        ]
        called_kan_indices = [
            self._string_to_136_tile(sou='1'),
            self._string_to_136_tile(sou='3'),
            self._string_to_136_tile(pin='5'),
            self._string_to_136_tile(man='2')
        ]

        result = hand.estimate_hand_value(
            tiles,
            win_tile,
            open_sets=open_sets,
            called_kan_indices=called_kan_indices)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 13)
        self.assertEqual(result['fu'], 80)
        self.assertEqual(len(result['hand_yaku']), 1)
Exemple #2
0
    def test_is_kokushi(self):
        hand = FinishedHand()

        tiles = self._string_to_34_array(sou='119',
                                         man='19',
                                         pin='19',
                                         honors='1234567')
        self.assertTrue(hand.is_kokushi(tiles))

        tiles = self._string_to_136_array(sou='119',
                                          man='19',
                                          pin='19',
                                          honors='1234567')
        win_tile = self._string_to_136_tile(sou='9')

        result = hand.estimate_hand_value(tiles, win_tile)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 13)
        self.assertEqual(result['fu'], 0)
        self.assertEqual(len(result['hand_yaku']), 1)

        tiles = self._string_to_136_array(sou='119',
                                          man='19',
                                          pin='19',
                                          honors='1234567')
        win_tile = self._string_to_136_tile(sou='1')

        result = hand.estimate_hand_value(tiles, win_tile)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 26)
        self.assertEqual(result['fu'], 0)
        self.assertEqual(len(result['hand_yaku']), 1)
Exemple #3
0
    def __init__(self, clients):
        self.tiles = []
        self.dead_wall = []
        self.dora_indicators = []
        self.clients = clients
        self._set_client_names()

        self.agari = Agari()
        self.finished_hand = FinishedHand()
Exemple #4
0
    def test_is_tenhou(self):
        hand = FinishedHand()

        tiles = self._string_to_136_array(sou='123444', man='234456', pin='66')
        win_tile = self._string_to_136_tile(sou='4')

        result = hand.estimate_hand_value(tiles, win_tile, is_tenhou=True)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 13)
        self.assertEqual(result['fu'], 40)
        self.assertEqual(len(result['hand_yaku']), 1)
Exemple #5
0
    def __init__(self, player):
        super(MainAI, self).__init__(player)

        self.agari = Agari()
        self.shanten = Shanten()
        self.defence = DefenceHandler(player)
        self.hand_divider = HandDivider()
        self.finished_hand = FinishedHand()
        self.previous_shanten = 7
        self.current_strategy = None
        self.waiting = []
        self.in_defence = False
Exemple #6
0
    def test_is_chinroto(self):
        hand = FinishedHand()

        tiles = self._string_to_34_array(sou='111999', man='111999', pin='99')
        self.assertTrue(hand.is_chinroto(self._hand(tiles, 0)))

        tiles = self._string_to_136_array(sou='111222', man='111999', pin='99')
        win_tile = self._string_to_136_tile(pin='9')

        result = hand.estimate_hand_value(tiles, win_tile)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 26)
        self.assertEqual(result['fu'], 60)
        self.assertEqual(len(result['hand_yaku']), 1)
Exemple #7
0
    def test_is_daisuushi(self):
        hand = FinishedHand()

        tiles = self._string_to_34_array(sou='22', honors='111222333444')
        self.assertTrue(hand.is_daisuushi(self._hand(tiles, 0)))

        tiles = self._string_to_136_array(sou='22', honors='111222333444')
        win_tile = self._string_to_136_tile(honors='4')

        result = hand.estimate_hand_value(tiles, win_tile)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 26)
        self.assertEqual(result['fu'], 60)
        self.assertEqual(len(result['hand_yaku']), 1)
Exemple #8
0
    def test_is_ryuisou(self):
        hand = FinishedHand()

        tiles = self._string_to_34_array(sou='22334466888', honors='666')
        self.assertTrue(hand.is_ryuisou(self._hand(tiles, 0)))

        tiles = self._string_to_136_array(sou='22334466888', honors='666')
        win_tile = self._string_to_136_tile(honors='6')

        result = hand.estimate_hand_value(tiles, win_tile)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 13)
        self.assertEqual(result['fu'], 40)
        self.assertEqual(len(result['hand_yaku']), 1)
Exemple #9
0
    def test_is_daisangen(self):
        hand = FinishedHand()

        tiles = self._string_to_34_array(sou='123',
                                         man='22',
                                         honors='555666777')
        self.assertTrue(hand.is_daisangen(self._hand(tiles, 0)))

        tiles = self._string_to_136_array(sou='123',
                                          man='22',
                                          honors='555666777')
        win_tile = self._string_to_136_tile(honors='7')

        result = hand.estimate_hand_value(tiles, win_tile)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 13)
        self.assertEqual(result['fu'], 50)
        self.assertEqual(len(result['hand_yaku']), 1)
Exemple #10
0
    def test_is_chuuren_poutou(self):
        hand = FinishedHand()

        tiles = self._string_to_34_array(man='11122345678999')
        self.assertTrue(hand.is_chuuren_poutou(self._hand(tiles, 0)))

        tiles = self._string_to_34_array(pin='11123345678999')
        self.assertTrue(hand.is_chuuren_poutou(self._hand(tiles, 0)))

        tiles = self._string_to_34_array(sou='11123456678999')
        self.assertTrue(hand.is_chuuren_poutou(self._hand(tiles, 0)))

        tiles = self._string_to_34_array(sou='11123456678999')
        self.assertTrue(hand.is_chuuren_poutou(self._hand(tiles, 0)))

        tiles = self._string_to_34_array(sou='11123456678999')
        self.assertTrue(hand.is_chuuren_poutou(self._hand(tiles, 0)))

        tiles = self._string_to_34_array(sou='11123456789999')
        self.assertTrue(hand.is_chuuren_poutou(self._hand(tiles, 0)))

        tiles = self._string_to_136_array(man='11123456789999')
        win_tile = self._string_to_136_tile(man='1')

        result = hand.estimate_hand_value(tiles, win_tile)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 13)
        self.assertEqual(result['fu'], 40)
        self.assertEqual(len(result['hand_yaku']), 1)

        tiles = self._string_to_136_array(man='11122345678999')
        win_tile = self._string_to_136_tile(man='2')

        result = hand.estimate_hand_value(tiles, win_tile)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 26)
        self.assertEqual(result['fu'], 50)
        self.assertEqual(len(result['hand_yaku']), 1)
Exemple #11
0
class CheckWaiting(object):
    def __init__(self):
        self.finished_hand = FinishedHand()

    def check(self,
              hand,
              win_tile,
              is_tsumo=False,
              is_riichi=False,
              is_dealer=False,
              is_ippatsu=False,
              is_rinshan=False,
              is_chankan=False,
              is_haitei=False,
              is_houtei=False,
              is_daburu_riichi=False,
              is_nagashi_mangan=False,
              is_tenhou=False,
              is_renhou=False,
              is_chiihou=False,
              open_sets=None,
              dora_indicators=None,
              called_kan_indices=None,
              player_wind=None,
              round_wind=None):
        result = self.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_nagashi_mangan=is_nagashi_mangan,
            is_tenhou=is_tenhou,
            is_renhou=is_renhou,
            is_chiihou=is_chiihou,
            open_sets=open_sets,
            dora_indicators=dora_indicators,
            called_kan_indices=called_kan_indices,
            player_wind=player_wind,
            round_wind=round_wind)
        return result
Exemple #12
0
    def test_is_suuankou(self):
        hand = FinishedHand()

        tiles = self._string_to_34_array(sou='111444', man='333', pin='44555')
        win_tile = self._string_to_136_tile(sou='4')

        self.assertTrue(hand.is_suuankou(win_tile, self._hand(tiles, 0), True))
        self.assertFalse(
            hand.is_suuankou(win_tile, self._hand(tiles, 0), False))

        tiles = self._string_to_136_array(sou='111444', man='333', pin='44555')
        win_tile = self._string_to_136_tile(pin='5')

        result = hand.estimate_hand_value(tiles, win_tile, is_tsumo=True)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 13)
        self.assertEqual(result['fu'], 50)
        self.assertEqual(len(result['hand_yaku']), 1)

        result = hand.estimate_hand_value(tiles, win_tile, is_tsumo=False)
        self.assertNotEqual(result['han'], 13)

        tiles = self._string_to_136_array(sou='111444', man='333', pin='44455')
        win_tile = self._string_to_136_tile(pin='5')

        result = hand.estimate_hand_value(tiles, win_tile, is_tsumo=True)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 26)
        self.assertEqual(result['fu'], 50)
        self.assertEqual(len(result['hand_yaku']), 1)

        tiles = self._string_to_136_array(man='33344455577799')
        win_tile = self._string_to_136_tile(man='9')

        result = hand.estimate_hand_value(tiles, win_tile, is_tsumo=False)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 26)
        self.assertEqual(result['fu'], 50)
        self.assertEqual(len(result['hand_yaku']), 1)
Exemple #13
0
    def test_is_tsuisou(self):
        hand = FinishedHand()

        tiles = self._string_to_34_array(honors='11122233366677')
        self.assertTrue(hand.is_tsuisou(self._hand(tiles, 0)))

        tiles = self._string_to_34_array(honors='11223344556677')
        self.assertTrue(hand.is_tsuisou(self._hand(tiles, 0)))

        tiles = self._string_to_34_array(honors='1133445577',
                                         pin='88',
                                         sou='11')
        self.assertFalse(hand.is_tsuisou(self._hand(tiles, 0)))

        tiles = self._string_to_136_array(honors='11223344556677')
        win_tile = self._string_to_136_tile(honors='7')

        result = hand.estimate_hand_value(tiles, win_tile)
        self.assertEqual(result['error'], None)
        self.assertEqual(result['han'], 13)
        self.assertEqual(result['fu'], 25)
        self.assertEqual(len(result['hand_yaku']), 1)
    def test_calculate_scores_and_ron(self):
        hand = FinishedHand()

        result = hand.calculate_scores(han=1,
                                       fu=30,
                                       is_tsumo=False,
                                       is_dealer=False)
        self.assertEqual(result['main'], 1000)

        result = hand.calculate_scores(han=1,
                                       fu=110,
                                       is_tsumo=False,
                                       is_dealer=False)
        self.assertEqual(result['main'], 3600)

        result = hand.calculate_scores(han=2,
                                       fu=30,
                                       is_tsumo=False,
                                       is_dealer=False)
        self.assertEqual(result['main'], 2000)

        result = hand.calculate_scores(han=3,
                                       fu=30,
                                       is_tsumo=False,
                                       is_dealer=False)
        self.assertEqual(result['main'], 3900)

        result = hand.calculate_scores(han=4,
                                       fu=30,
                                       is_tsumo=False,
                                       is_dealer=False)
        self.assertEqual(result['main'], 7700)

        result = hand.calculate_scores(han=4,
                                       fu=40,
                                       is_tsumo=False,
                                       is_dealer=False)
        self.assertEqual(result['main'], 8000)

        result = hand.calculate_scores(han=4,
                                       fu=40,
                                       is_tsumo=False,
                                       is_dealer=True)
        self.assertEqual(result['main'], 12000)

        result = hand.calculate_scores(han=5,
                                       fu=0,
                                       is_tsumo=False,
                                       is_dealer=False)
        self.assertEqual(result['main'], 8000)

        result = hand.calculate_scores(han=6,
                                       fu=0,
                                       is_tsumo=False,
                                       is_dealer=False)
        self.assertEqual(result['main'], 12000)

        result = hand.calculate_scores(han=8,
                                       fu=0,
                                       is_tsumo=False,
                                       is_dealer=False)
        self.assertEqual(result['main'], 16000)

        result = hand.calculate_scores(han=11,
                                       fu=0,
                                       is_tsumo=False,
                                       is_dealer=False)
        self.assertEqual(result['main'], 24000)

        result = hand.calculate_scores(han=13,
                                       fu=0,
                                       is_tsumo=False,
                                       is_dealer=False)
        self.assertEqual(result['main'], 32000)

        result = hand.calculate_scores(han=26,
                                       fu=0,
                                       is_tsumo=False,
                                       is_dealer=False)
        self.assertEqual(result['main'], 64000)
    def test_calculate_scores_and_tsumo_by_dealer(self):
        hand = FinishedHand()

        result = hand.calculate_scores(han=1,
                                       fu=30,
                                       is_tsumo=True,
                                       is_dealer=True)
        self.assertEqual(result['main'], 500)
        self.assertEqual(result['additional'], 500)

        result = hand.calculate_scores(han=3,
                                       fu=30,
                                       is_tsumo=True,
                                       is_dealer=True)
        self.assertEqual(result['main'], 2000)
        self.assertEqual(result['additional'], 2000)

        result = hand.calculate_scores(han=4,
                                       fu=30,
                                       is_tsumo=True,
                                       is_dealer=True)
        self.assertEqual(result['main'], 3900)
        self.assertEqual(result['additional'], 3900)

        result = hand.calculate_scores(han=5,
                                       fu=0,
                                       is_tsumo=True,
                                       is_dealer=True)
        self.assertEqual(result['main'], 4000)
        self.assertEqual(result['additional'], 4000)

        result = hand.calculate_scores(han=6,
                                       fu=0,
                                       is_tsumo=True,
                                       is_dealer=True)
        self.assertEqual(result['main'], 6000)
        self.assertEqual(result['additional'], 6000)

        result = hand.calculate_scores(han=8,
                                       fu=0,
                                       is_tsumo=True,
                                       is_dealer=True)
        self.assertEqual(result['main'], 8000)
        self.assertEqual(result['additional'], 8000)

        result = hand.calculate_scores(han=11,
                                       fu=0,
                                       is_tsumo=True,
                                       is_dealer=True)
        self.assertEqual(result['main'], 12000)
        self.assertEqual(result['additional'], 12000)

        result = hand.calculate_scores(han=13,
                                       fu=0,
                                       is_tsumo=True,
                                       is_dealer=True)
        self.assertEqual(result['main'], 16000)
        self.assertEqual(result['additional'], 16000)

        result = hand.calculate_scores(han=26,
                                       fu=0,
                                       is_tsumo=True,
                                       is_dealer=True)
        self.assertEqual(result['main'], 32000)
        self.assertEqual(result['additional'], 32000)
Exemple #16
0
 def __init__(self):
     self.finished_hand = FinishedHand()
Exemple #17
0
class GameManager(object):
    """
    Allow to play bots between each other
    To have a metrics how new version plays agains old versions
    """

    tiles = []
    dead_wall = []
    clients = []
    dora_indicators = []

    dealer = None
    current_client = None
    round_number = 0
    honba_sticks = 0
    riichi_sticks = 0

    _unique_dealers = 0

    def __init__(self, clients):
        self.tiles = []
        self.dead_wall = []
        self.dora_indicators = []
        self.clients = clients
        self._set_client_names()

        self.agari = Agari()
        self.finished_hand = FinishedHand()

    def init_game(self):
        """
        Initial of the game.
        Clients random placement and dealer selection
        """
        shuffle(self.clients, shuffle_seed)
        for i in range(0, len(self.clients)):
            self.clients[i].position = i

        dealer = randint(0, 3)
        self.set_dealer(dealer)

        for client in self.clients:
            client.player.scores = 25000

        self._unique_dealers = 1

    def init_round(self):
        # each round should have personal seed
        global seed_value
        seed_value = random()

        self.tiles = [i for i in range(0, 136)]

        # need to change random function in future
        shuffle(self.tiles, shuffle_seed)

        self.dead_wall = self._cut_tiles(14)
        self.dora_indicators.append(self.dead_wall[8])

        for x in range(0, len(self.clients)):
            client = self.clients[x]

            # each client think that he is a player with position = 0
            # so, we need to move dealer position for each client
            # and shift scores array
            client_dealer = self.dealer - x

            player_scores = deque([i.player.scores / 100 for i in self.clients])
            player_scores.rotate(x * -1)
            player_scores = list(player_scores)

            client.table.init_round(
                self.round_number,
                self.honba_sticks,
                self.riichi_sticks,
                self.dora_indicators[0],
                client_dealer,
                player_scores
            )

        # each player by rotation draw 4 tiles until they have 12
        # after this each player draw one more tile
        # and this is will be their initial hand
        # we do it to make the tiles allocation in hands
        # more random
        for x in range(0, 3):
            for client in self.clients:
                client.player.tiles += self._cut_tiles(4)

        for client in self.clients:
            client.player.tiles += self._cut_tiles(1)
            client.init_hand(client.player.tiles)

        logger.info('Seed: {0}'.format(shuffle_seed()))
        logger.info('Dealer: {0}'.format(self.dealer))
        logger.info('Wind: {0}. Riichi sticks: {1}. Honba sticks: {2}'.format(
            self._unique_dealers,
            self.riichi_sticks,
            self.honba_sticks
        ))
        logger.info('Players: {0}'.format(self.players_sorted_by_scores()))

    def play_round(self):
        continue_to_play = True

        while continue_to_play:
            client = self._get_current_client()
            in_tempai = client.player.in_tempai

            tile = self._cut_tiles(1)[0]

            # we don't need to add tile to the hand when we are in riichi
            if client.player.in_riichi:
                tiles = client.player.tiles + [tile]
            else:
                client.draw_tile(tile)
                tiles = client.player.tiles

            is_win = self.agari.is_agari(TilesConverter.to_34_array(tiles))

            # win by tsumo after tile draw
            if is_win:
                result = self.process_the_end_of_the_round(tiles=client.player.tiles,
                                                           win_tile=tile,
                                                           winner=client,
                                                           loser=None,
                                                           is_tsumo=True)
                return result

            # if not in riichi, let's decide what tile to discard
            if not client.player.in_riichi:
                tile = client.discard_tile()
                in_tempai = client.player.in_tempai

            # after tile discard let's check all other players can they win or not
            # at this tile
            for other_client in self.clients:
                # there is no need to check the current client
                if other_client == client:
                    continue

                # let's store other players discards
                other_client.enemy_discard(other_client.position - client.position, tile)

                # TODO support multiple ron
                if self.can_call_ron(other_client, tile):
                    # the end of the round
                    result = self.process_the_end_of_the_round(tiles=other_client.player.tiles,
                                                               win_tile=tile,
                                                               winner=other_client,
                                                               loser=client,
                                                               is_tsumo=False)
                    return result

            # if there is no challenger to ron, let's check can we call riichi with tile discard or not
            if in_tempai and client.player.can_call_riichi():
                self.call_riichi(client)

            self.current_client = self._move_position(self.current_client)

            # retake
            if not len(self.tiles):
                continue_to_play = False

        result = self.process_the_end_of_the_round([], 0, None, None, False)
        return result

    def play_game(self, total_results):
        """
        :param total_results: a dictionary with keys as client ids
        :return: game results
        """
        logger.info('The start of the game')
        logger.info('')

        is_game_end = False
        self.init_game()

        played_rounds = 0

        while not is_game_end:
            self.init_round()
            result = self.play_round()

            is_game_end = result['is_game_end']
            loser = result['loser']
            winner = result['winner']
            if loser:
                total_results[loser.id]['lose_rounds'] += 1
            if winner:
                total_results[winner.id]['win_rounds'] += 1

            for client in self.clients:
                if client.player.in_riichi:
                    total_results[client.id]['riichi_rounds'] += 1

            played_rounds += 1

        self.recalculate_players_position()

        logger.info('Final Scores: {0}'.format(self.players_sorted_by_scores()))
        logger.info('The end of the game')

        return {'played_rounds': played_rounds}

    def recalculate_players_position(self):
        """
        For players with same count of scores we need
        to set position based on their initial seat on the table
        """
        temp_clients = sorted(self.clients, key=lambda x: x.player.scores, reverse=True)
        for i in range(0, len(temp_clients)):
            temp_client = temp_clients[i]

            for client in self.clients:
                if client.id == temp_client.id:
                    client.player.position = i + 1

    def can_call_ron(self, client, win_tile):
        if not client.player.in_tempai or not client.player.in_riichi:
            return False
        tiles = client.player.tiles
        is_ron = self.agari.is_agari(TilesConverter.to_34_array(tiles + [win_tile]))
        return is_ron

    def call_riichi(self, client):
        client.player.in_riichi = True
        client.player.scores -= 1000
        self.riichi_sticks += 1

        who_called_riichi = client.position
        for client in self.clients:
            client.enemy_riichi(who_called_riichi - client.position)

        logger.info('Riichi: {0} - 1,000'.format(client.player.name))

    def set_dealer(self, dealer):
        self.dealer = dealer
        self._unique_dealers += 1

        for x in range(0, len(self.clients)):
            client = self.clients[x]

            # each client think that he is a player with position = 0
            # so, we need to move dealer position for each client
            # and shift scores array
            client.player.dealer_seat = dealer - x

        # first move should be dealer's move
        self.current_client = dealer

    def process_the_end_of_the_round(self, tiles, win_tile, winner, loser, is_tsumo):
        """
        Increment a round number and do a scores calculations
        """

        if winner:
            logger.info('{0}: {1} + {2}'.format(
                is_tsumo and 'Tsumo' or 'Ron',
                TilesConverter.to_one_line_string(tiles),
                TilesConverter.to_one_line_string([win_tile])),
            )
        else:
            logger.info('Retake')

        is_game_end = False
        self.round_number += 1

        if winner:
            hand_value = self.finished_hand.estimate_hand_value(tiles + [win_tile],
                                                                win_tile,
                                                                is_tsumo,
                                                                winner.player.in_riichi,
                                                                winner.player.is_dealer,
                                                                False)
            if hand_value['cost']:
                hand_value = hand_value['cost']['main']
            else:
                logger.error('Can\'t estimate a hand: {0}. Error: {1}'.format(
                    TilesConverter.to_one_line_string(tiles + [win_tile]),
                    hand_value['error']
                ))
                hand_value = 1000

            scores_to_pay = hand_value + self.honba_sticks * 300
            riichi_bonus = self.riichi_sticks * 1000
            self.riichi_sticks = 0

            # if dealer won we need to increment honba sticks
            if winner.player.is_dealer:
                self.honba_sticks += 1
            else:
                self.honba_sticks = 0
                new_dealer = self._move_position(self.dealer)
                self.set_dealer(new_dealer)

            # win by ron
            if loser:
                win_amount = scores_to_pay + riichi_bonus
                winner.player.scores += win_amount
                loser.player.scores -= scores_to_pay

                logger.info('Win:  {0} + {1:,d}'.format(winner.player.name, win_amount))
                logger.info('Lose: {0} - {1:,d}'.format(loser.player.name, scores_to_pay))
            # win by tsumo
            else:
                scores_to_pay /= 3
                # will be changed when real hand calculation will be implemented
                # round to nearest 100. 333 -> 300
                scores_to_pay = 100 * round(float(scores_to_pay) / 100)

                win_amount = scores_to_pay * 3 + riichi_bonus
                winner.player.scores += win_amount

                for client in self.clients:
                    if client != winner:
                        client.player.scores -= scores_to_pay

                logger.info('Win: {0} + {1:,d}'.format(winner.player.name, win_amount))
        # retake
        else:
            tempai_users = 0

            for client in self.clients:
                if client.player.in_tempai:
                    tempai_users += 1

            if tempai_users == 0 or tempai_users == 4:
                self.honba_sticks += 1
                # no one in tempai, so deal should move
                if tempai_users == 0:
                    new_dealer = self._move_position(self.dealer)
                    self.set_dealer(new_dealer)
            else:
                # 1 tempai user  will get 3000
                # 2 tempai users will get 1500 each
                # 3 tempai users will get 1000 each
                scores_to_pay = 3000 / tempai_users
                for client in self.clients:
                    if client.player.in_tempai:
                        client.player.scores += scores_to_pay
                        logger.info('{0} + {1:,d}'.format(client.player.name, int(scores_to_pay)))

                        # dealer was tempai, we need to add honba stick
                        if client.player.is_dealer:
                            self.honba_sticks += 1
                    else:
                        client.player.scores -= 3000 / (4 - tempai_users)

        # if someone has negative scores,
        # we need to end the game
        for client in self.clients:
            if client.player.scores < 0:
                is_game_end = True

        # we have played all 8 winds, let's finish the game
        if self._unique_dealers > 8:
            is_game_end = True

        logger.info('')

        return {
            'winner': winner,
            'loser': loser,
            'is_tsumo': is_tsumo,
            'is_game_end': is_game_end
        }

    def players_sorted_by_scores(self):
        return sorted([i.player for i in self.clients], key=lambda x: x.scores, reverse=True)

    def _set_client_names(self):
        """
        For better tests output
        """
        names = ['Sato', 'Suzuki', 'Takahashi', 'Tanaka', 'Watanabe', 'Ito',
                 'Yamamoto', 'Nakamura', 'Kobayashi', 'Kato', 'Yoshida', 'Yamada']

        for client in self.clients:
            name = names[randint(0, len(names) - 1)]
            names.remove(name)
            from mahjong.myAI.SLCNNPlayer import SLCNNPlayer
            print(isinstance(client.player.ai, SLCNNPlayer))
            #if isinstance(client.player.ai, SLCNNPlayer):
            #    client.player.name = "Suu"
            #else:
            #    client.player.name = name

    def _get_current_client(self) -> Client:
        return self.clients[self.current_client]

    def _cut_tiles(self, count_of_tiles) -> []:
        """
        Cut the tiles array
        :param count_of_tiles: how much tiles to cut
        :return: the array with specified count of tiles
        """

        result = self.tiles[0:count_of_tiles]
        self.tiles = self.tiles[count_of_tiles:len(self.tiles)]
        return result

    def _move_position(self, current_position):
        """
        loop 0 -> 1 -> 2 -> 3 -> 0
        """
        current_position += 1
        if current_position > 3:
            current_position = 0
        return current_position
Exemple #18
0
class MonteCarlo(object):
    """A simulator to run Monte Carlo simulation on Mahjong game
    """
    decoder = TenhouDecoder()
    agari = Agari()
    finished_hand = FinishedHand()
    hand_calculator = HandCalculator()
    verbose = False

    def __init__(self):

        previous_ai = False
        table = MCVisibleTable(previous_ai)
        self.table = table

        self.initialize()

        # We load the classifiers and regressors here
        self.clf_one_player = []
        for n in range(34):
            clf = pickle.load(
                open(
                    abs_data_path +
                    "/train_model/trained_models/one_player_{}.sav".format(n),
                    "rb"))
            self.clf_one_player.append(clf)

    def initialize(self):
        # Mahjong table is ready
        #        previous_ai = False
        #        table = MCVisibleTable(previous_ai)
        #        self.table = table
        self.table.erase_state()
        self.table.turn_number = [0, 0, 0, 0]

        # Prepare a new deck of mahjong tiles
        tiles = list(range(136))
        # shuffle the tiles
        random.shuffle(tiles)
        # seperate dead wall and dora indicator
        dead_wall = tiles[-14:]
        dora_indicator = dead_wall[4]  # random.choice(dead_wall)
        self.table.dead_wall = dead_wall
        # generate init hands for 4 players
        tiles = tiles[0:-14]
        hands = []
        for n in range(4):
            init_tiles = tiles[0:13]
            hands.append(init_tiles)
            tiles = tiles[13:]

        self.table.remaining_tiles = tiles

        # formulate the init message
        dice1 = random.choice([1, 2, 3, 4, 5, 6])
        dice2 = random.choice([1, 2, 3, 4, 5, 6])
        dealer = (dice1 - 1 + dice2 - 1) % 4
        message = "<INIT "
        # TODO: You may need round number, number of combo sticks, and number of
        # riichi sticks other than 0?
        message += 'seed="{},{},{},{},{},{}" '.format(0, 0, 0, dice1 - 1,
                                                      dice2 - 1,
                                                      dora_indicator)
        message += 'ten="250,250,250,250" '
        message += 'oya="{}" '.format(dealer)
        message += 'hai0="{}" '.format(",".join(str(t) for t in hands[0]))
        message += 'hai1="{}" '.format(",".join(str(t) for t in hands[1]))
        message += 'hai2="{}" '.format(",".join(str(t) for t in hands[2]))
        message += 'hai3="{}"/>'.format(",".join(str(t) for t in hands[3]))

        # once message is ready, we decoder the info and initialize the table
        values = self.decoder.parse_initial_values(message)
        self.table.init_round(
            values['round_number'],
            values['count_of_honba_sticks'],
            values['count_of_riichi_sticks'],
            values['dora_indicator'],
            values['dealer'],
            values['scores'],
        )
        # TODO: this part is unnecessary, since we have already the hands list
        hands = [
            [
                int(x) for x in self.decoder.get_attribute_content(
                    message, 'hai0').split(',')
            ],
            [
                int(x) for x in self.decoder.get_attribute_content(
                    message, 'hai1').split(',')
            ],
            [
                int(x) for x in self.decoder.get_attribute_content(
                    message, 'hai2').split(',')
            ],
            [
                int(x) for x in self.decoder.get_attribute_content(
                    message, 'hai3').split(',')
            ],
        ]
        # Initialize all players on the table
        # TODO: ok, we always assume we are sitting at seat 0
        self.player_position = 0
        self.table.players[0].init_hand(hands[self.player_position])
        self.table.players[1].init_hand(hands[(self.player_position + 1) % 4])
        self.table.players[2].init_hand(hands[(self.player_position + 2) % 4])
        self.table.players[3].init_hand(hands[(self.player_position + 3) % 4])
        #        main_player = self.table.player
        #        print(self.table.__str__())
        #        print('Players: {}'.format(self.table.get_players_sorted_by_scores()))
        #        print('Dealer: {}'.format(self.table.get_player(values['dealer'])))
        #        print('Round  wind: {}'.format(DISPLAY_WINDS[self.table.round_wind]))
        #        print('Player wind: {}'.format(DISPLAY_WINDS[main_player.player_wind]))

        # TODO: this part may not be necessary. Basically we need to erase the
        # melds information since it is a new game.
        # If we are using opponent_model, we need to reset the melds
        # when a new game initiated. For other model, this function
        # may not exist. [Joseph]
        try:
            self.table.player.ai.reset_melds()
        except:
            pass

    def _check_win(self, player_hand, melds):
        """ check the hand to see whether it's a win hand or not
        :param player_hand: list of int (0-33), the player's hand tiles in 34 format
        :return: True if win, else False.
        """
        return self.agari.is_agari(player_hand, melds)

    def check_win(self, p):
        """check win hand
        :param p: int (0-3), player index
        :return: True if win, False if not.
        """
        player_hand = self.table.players[p].tiles
        player_hand_34 = TilesConverter.to_34_array(player_hand)
        melds = [meld.tiles for meld in self.table.players[p].melds
                 ]  # self.table.players[p].melds is [list of Meld()]
        melds_34 = list(map(lambda lst: [l // 4 for l in lst], melds))
        return self._check_win(player_hand_34, melds_34)

    def _check_waiting(self, player_hand):
        """Check whether a player is waiting or not based on his hand
        :param player_hand: list of elements in [0,133], the hand tiles of player
        :return: True for waiting, False for not waiting
        """
        # If we need one more tile to complete our hand, and this specific tile
        # we want is known to be within the wall tiles, then the hand is waiting.
        current_hand = TilesConverter.to_34_array(player_hand)
        #winning_tiles = []
        for n in range(34):
            table_revealed_tiles_34 = self.table.revealed_tiles
            if table_revealed_tiles_34[n] < 4:
                completed_hand = current_hand[:]
                completed_hand[n] += 1
                can_be_waiting = self.agari.is_agari(completed_hand)
                if can_be_waiting:
                    return True
                    #winning_tiles.append(n) # n is the winning tile we want

        # If there exists winnint tiles, the player is waitinng
        #if len(winning_tiles)>0:
        #    return True
        return False

    def check_waiting(self, p):
        """check whether the player `p` is waiting or not
        :param p: int (0-3), player index
        :return: True for waiting, False for not waiting
        """
        player_hand = self.table.players[p].tiles
        return self._check_waiting(player_hand)

    def discard_tile(self, p):
        """choose a tile to discard based on hand tiles of player `p`
        :param p: int(0-3), player index
        :return: int(0-135), tile to discard
        """
        tiles = self.table.players[p].tiles
        closed_hand = self.table.players[p].closed_hand
        open_sets_34 = self.table.players[p].open_hand_34_tiles
        table_revealed_tiles_34 = self.table.revealed_tiles
        discarded_tile = self.table.players[p].ai.discard_tile(
            tiles, closed_hand, open_sets_34, table_revealed_tiles_34)
        return discarded_tile

    def call_meld(self, type, who, from_who, opened, tiles, called_tile):
        meld = Meld()
        meld.type = type
        meld.who = who
        meld.from_who = from_who
        meld.opened = opened
        meld.tiles = tiles
        meld.called_tile = called_tile
        return meld

    def sim_WPET(self):
        """Simulation of a riichi mahjong game, and record the bw and r parameters
        """
        # We don't need to fix the seed now.
        #seed = random.randint(1,100000)
        #seed = 73823
        #random.seed(seed)

        bw_riichi = 0
        bw_stealing = 0
        r = 0

        # start a new game
        self.initialize()
        # rinshan
        rinshan = self.table.dead_wall[0:4]
        # start from dealer
        p = self.table.dealer_seat
        #logger.info("seed: %d"%seed)
        logger.info("dealer seat: %d" % p)
        # Fixed: self.table.count_of_remaining_tiles>0 may not be accurate here
        while self.table.count_of_remaining_tiles > 0:  #len(self.table.remaining_tiles)>0:

            # our perspective of view is from the program's turn
            if p == 0:
                r += 1

            if self.check_waiting(0):
                # TODO: Why don't we use in_riichi here?
                if self.table.players[0].is_open_hand:
                    bw_stealing = 1
                else:
                    bw_riichi = 1
                break

            logger.debug("[begin]:%s,%s" %
                         (self.table.count_of_remaining_tiles,
                          len(self.table.remaining_tiles)))
            # p draw a tile
            drawn_tile = self.table.remaining_tiles[0]
            self.table.players[p].draw_tile(drawn_tile)
            # check p win
            if self.check_win(p):
                logger.info("player {} wins (tsumo)!".format(p))
                break

            # TODO: we only apply discard_tile strategy to our program, for the
            # opponent players, we assume they simply discard what they draw
            # if not win, we do the followings
            if True:  #p==0:
                if not self.table.players[p].in_riichi:
                    # choose a discard tile for playe `p`
                    discarded_tile = self.discard_tile(p)
                    # see if we can call riichi
                    if self.table.players[p].can_call_riichi():
                        logger.debug("player {} call riichi.".format(p))
                        self.table.players[p].in_riichi = True
                else:  # if riichi, we have to discard whatever we draw
                    discarded_tile = drawn_tile
            else:
                discarded_tile = drawn_tile

            # remove the tile from player's hand
            discarded_tile_tmp = discarded_tile
            drawn_tile_tmp = drawn_tile
            is_tsumogiri = (discarded_tile == drawn_tile)
            self.table.players[p].tiles.remove(discarded_tile)
            logger.debug("player {} discards {}".format(p, discarded_tile))
            logger.debug("\tclosed hand: %s" %
                         self.table.players[0].closed_hand)
            logger.debug("\topen hand: %s" %
                         self.table.players[0].open_hand_34_tiles)
            logger.debug("\tmeld tiles: %s" % self.table.players[0].meld_tiles)
            # now we check call meld
            # TODO: Ok, here we only allow our program to call meld. But maybe
            # we should allow the opponents to call meld too?
            if p != 0:
                previous_drawn_tile = drawn_tile
                tile = discarded_tile
                is_kamicha_discard = (p == 3)
                meld, discard_option = self.table.players[0].try_to_call_meld(
                    tile, is_kamicha_discard)
                kan_type = self.table.players[0].can_call_kan(tile, True)
                if kan_type:  # kan or chankan
                    tiles = [(tile // 4) * 4, (tile // 4) * 4 + 1,
                             (tile // 4) * 4 + 2, (tile // 4) * 4 + 3]
                    meld = self.call_meld(kan_type, 0, p, True, tiles, tile)
                    logger.debug("player 0 call kan from %d: %s" % (p, meld))
                    player_seat = meld.who
                    self.table.add_called_meld(player_seat, meld)
                    # we had to delete called tile from hand
                    # to have correct tiles count in the hand
                    # TODO[joseph]: I still don't know why we need this
                    # if meld type is: chi, pon, or nuki
                    # maybe no use here, b/c the meld type is always Meld.KAN here
                    if meld.type != Meld.KAN and meld.type != Meld.CHANKAN:
                        self.table.players[player_seat].draw_tile(
                            meld.called_tile)
                    # draw a tile from dead wall
                    if len(rinshan) > 0:
                        drawn_tile = rinshan.pop(0)
                        self.table.players[0].draw_tile(drawn_tile)
                        # check p win
                        if self.check_win(0):
                            logger.info(
                                "player {} wins (rinshan tsumo)!".format(0))
                            break
                        # if not win, we do the followings
                        if not self.table.players[0].in_riichi:
                            # choose a discard tile for playe `p`
                            discarded_tile = self.discard_tile(0)
                            # see if we can call riichi
                            if self.table.players[0].can_call_riichi():
                                logger.info("player {} call riichi.".format(0))
                                self.table.players[0].in_riichi = True
                        else:  # if riichi, we have to discard whatever we draw
                            discarded_tile = drawn_tile
                        # remove the tile from player's hand
                        self.table.players[0].tiles.remove(discarded_tile)
                        logger.debug("player {} discards {} after kan".format(
                            0, discarded_tile))
                        logger.debug("\tclosed hand: %s" %
                                     self.table.players[0].closed_hand)
                        logger.debug("\topen hand: %s" %
                                     self.table.players[0].open_hand_34_tiles)
                        logger.debug("\tmeld tiles: %s" %
                                     self.table.players[0].meld_tiles)
                        # we had to add it to discards, to calculate remaining tiles correctly
                        # drawn tile is not the one drawn from rinshan, but
                        # the one previously discarded by player `p`
                        self.table.add_discarded_tile(0, discarded_tile, True,
                                                      previous_drawn_tile)
                        # after program discarding a card, next player is 1
                        p = 1
                        continue
                    else:  # ryuukyoku
                        logger.debug("Rinshan empty. Ryuukyoku!")
                        break
                elif meld:  # pon, chi
                    logger.debug("player 0 %s from %d: %s" %
                                 (meld.type, p, meld))
                    player_seat = 0
                    # DEBUG: we change the add_called_meld method, delete the
                    # part that changes self.table.count_of_remaining_tiles
                    self.table.add_called_meld(player_seat, meld)
                    # Equivalently, program draws the tile discarded by opponent
                    self.table.players[0].draw_tile(tile)
                    # check p win
                    if self.check_win(0):
                        logger.info("player {} wins (by {})!".format(
                            0, meld.type))
                        break
                    # if not win, we do the followings
                    if not self.table.players[0].in_riichi:
                        # choose a discard tile for playe `p`
                        discarded_tile = self.discard_tile(0)
                        # see if we can call riichi
                        if self.table.players[0].can_call_riichi():
                            logger.debug("player {} call riichi.".format(0))
                            self.table.players[0].in_riichi = True
                    else:  # if riichi, we can not call meld
                        raise ("Riichi player can not call meld!")
                    # remove the tile from player's hand
                    self.table.players[0].tiles.remove(discarded_tile)
                    # discarded tile added to table
                    self.table.add_discarded_tile(0, discarded_tile, True,
                                                  previous_drawn_tile)
                    logger.debug("player {} discards {} after {}".format(
                        0, discarded_tile, meld.type))
                    logger.debug("\tclosed hand: %s" %
                                 self.table.players[0].closed_hand)
                    logger.debug("\topen hand: %s" %
                                 self.table.players[0].open_hand_34_tiles)
                    logger.debug("\tmeld tiles: %s" %
                                 self.table.players[0].meld_tiles)
                    # after program discarding a card, next player is 1
                    p = 1
                    continue
            # we had to add it to discards, to calculate remaining tiles correctly
            self.table.add_discarded_tile(p, discarded_tile_tmp, is_tsumogiri,
                                          drawn_tile_tmp)

            # next player
            p = (p + 1) % 4

            logger.debug("[after]:%s,%s" %
                         (self.table.count_of_remaining_tiles,
                          len(self.table.remaining_tiles)))

        # output results
        logger.debug('\n')
        for p in range(4):
            logger.info("\tPlayer %d: %s (%s)" %
                        (p,
                         TilesConverter.to_one_line_string(
                             self.table.players[p].tiles),
                         TilesConverter.to_one_line_string(
                             self.table.players[p].closed_hand)))

        return bw_riichi, bw_stealing, r

    def get_WPET(self, Nsim):
        """Waiting probability at Each Turn
        """
        R = [0] * 19
        BW_riichi = [0] * 19
        BW_stealing = [0] * 19
        n = 0
        while n < Nsim:
            bw_riichi, bw_stealing, r = self.sim_WPET()
            print("%d. bw_riichi=%s, bw_stealing=%s, r=%s" %
                  (n + 1, bw_riichi, bw_stealing, r))
            for m in range(r + 1):  # from 0 to r
                R[m] += 1
            BW_riichi[r] += bw_riichi
            BW_stealing[r] += bw_stealing
            n += 1
        Wpet_riichi = [
            BW_riichi[n] * 1.0 / R[n] for n in range(len(BW_riichi))
        ]
        Wpet_stealing = [
            BW_stealing[n] * 1.0 / R[n] for n in range(len(BW_stealing))
        ]
        return BW_riichi, BW_stealing, R, Wpet_riichi, Wpet_stealing

    def sim_game(self):
        """Simulation of a riichi mahjong game, and record the bw and r parameters
        """
        # We don't need to fix the seed now.
        #seed = random.randint(1,100000)
        #seed = 73823
        #random.seed(seed)

        # start a new game
        self.initialize()
        # rinshan
        rinshan = self.table.dead_wall[0:4]
        # start from dealer
        p = self.table.dealer_seat
        #logger.info("seed: %d"%seed)
        logger.info("dealer seat: %d" % p)
        # Fixed: self.table.count_of_remaining_tiles>0 may not be accurate here
        while self.table.count_of_remaining_tiles > 0:  #len(self.table.remaining_tiles)>0:

            logger.debug("[begin]:%s,%s" %
                         (self.table.count_of_remaining_tiles,
                          len(self.table.remaining_tiles)))

            # update turn number
            self.table.turn_number[p] += 1

            # p draw a tile
            drawn_tile = self.table.remaining_tiles[0]
            self.table.players[p].draw_tile(drawn_tile)
            # check p win
            if self.check_win(p):
                logger.info("player {} wins (tsumo)!".format(p))
                tiles = self.table.players[p].tiles
                win_tile = drawn_tile
                (is_tsumo, is_riichi, is_dealer, open_sets, dora_indicators,
                 player_wind, round_wind) = self.check_status(p)
                ## TODO: Wait to be finished!
                result = self.finished_hand.estimate_hand_value(
                    tiles,
                    win_tile,
                    is_tsumo=is_tsumo,
                    is_riichi=is_riichi,
                    is_dealer=is_dealer,
                    is_ippatsu=False,
                    is_rinshan=False,
                    is_chankan=False,
                    is_haitei=False,
                    is_houtei=False,
                    is_daburu_riichi=False,
                    is_nagashi_mangan=False,
                    is_tenhou=False,
                    is_renhou=False,
                    is_chiihou=False,
                    open_sets=open_sets,
                    dora_indicators=dora_indicators,
                    called_kan_indices=None,
                    player_wind=player_wind,
                    round_wind=round_wind)
                logger.info(result)

                melds = self.table.players[p].melds
                result = self.hand_calculator.estimate_hand_value(
                    tiles,
                    win_tile,
                    melds=melds,
                    dora_indicators=dora_indicators,
                    config=HandConfig(is_tsumo=is_tsumo,
                                      is_riichi=is_riichi,
                                      player_wind=player_wind,
                                      round_wind=round_wind))
                logger.info(result)
                break

            # TODO: we only apply discard_tile strategy to our program, for the
            # opponent players, we assume they simply discard what they draw
            # if not win, we do the followings
            if True:  #p==0:
                if not self.table.players[p].in_riichi:
                    # choose a discard tile for playe `p`
                    discarded_tile = self.discard_tile(p)
                    # see if we can call riichi
                    if self.table.players[p].can_call_riichi():
                        logger.debug("player {} call riichi.".format(p))
                        self.table.players[p].in_riichi = True
                else:  # if riichi, we have to discard whatever we draw
                    discarded_tile = drawn_tile
            else:
                discarded_tile = drawn_tile

            # remove the tile from player's hand
            discarded_tile_tmp = discarded_tile
            drawn_tile_tmp = drawn_tile
            is_tsumogiri = (discarded_tile == drawn_tile)
            self.table.players[p].tiles.remove(discarded_tile)
            logger.debug("player {} discards {}".format(p, discarded_tile))
            logger.debug("\tclosed hand: %s" %
                         self.table.players[0].closed_hand)
            logger.debug("\topen hand: %s" %
                         self.table.players[0].open_hand_34_tiles)
            logger.debug("\tmeld tiles: %s" % self.table.players[0].meld_tiles)
            # now we check call meld
            # TODO: Ok, here we only allow our program to call meld. But maybe
            # we should allow the opponents to call meld too?
            if p != 0:
                previous_drawn_tile = drawn_tile
                tile = discarded_tile
                is_kamicha_discard = (p == 3)
                meld, discard_option = self.table.players[0].try_to_call_meld(
                    tile, is_kamicha_discard)
                kan_type = self.table.players[0].can_call_kan(tile, True)
                if kan_type:  # kan or chankan
                    tiles = [(tile // 4) * 4, (tile // 4) * 4 + 1,
                             (tile // 4) * 4 + 2, (tile // 4) * 4 + 3]
                    meld = self.call_meld(kan_type, 0, p, True, tiles, tile)
                    logger.debug("player 0 call kan from %d: %s" % (p, meld))
                    player_seat = meld.who
                    self.table.add_called_meld(player_seat, meld)
                    # we had to delete called tile from hand
                    # to have correct tiles count in the hand
                    # TODO[joseph]: I still don't know why we need this
                    # if meld type is: chi, pon, or nuki
                    # maybe no use here, b/c the meld type is always Meld.KAN here
                    if meld.type != Meld.KAN and meld.type != Meld.CHANKAN:
                        self.table.players[player_seat].draw_tile(
                            meld.called_tile)
                    # draw a tile from dead wall
                    if len(rinshan) > 0:
                        drawn_tile = rinshan.pop(0)
                        self.table.players[0].draw_tile(drawn_tile)
                        # check p win
                        if self.check_win(0):
                            logger.info(
                                "player {} wins (rinshan tsumo)!".format(0))
                            tiles = self.table.players[p].tiles
                            win_tile = drawn_tile
                            (is_tsumo, is_riichi, is_dealer, open_sets,
                             dora_indicators, player_wind,
                             round_wind) = self.check_status(p)
                            is_rinshan = True
                            ## TODO: Wait to be finished!
                            result = self.finished_hand.estimate_hand_value(
                                tiles,
                                win_tile,
                                is_tsumo=is_tsumo,
                                is_riichi=is_riichi,
                                is_dealer=is_dealer,
                                is_ippatsu=False,
                                is_rinshan=is_rinshan,
                                is_chankan=False,
                                is_haitei=False,
                                is_houtei=False,
                                is_daburu_riichi=False,
                                is_nagashi_mangan=False,
                                is_tenhou=False,
                                is_renhou=False,
                                is_chiihou=False,
                                open_sets=open_sets,
                                dora_indicators=dora_indicators,
                                called_kan_indices=None,
                                player_wind=player_wind,
                                round_wind=round_wind)
                            logger.info(result)

                            melds = self.table.players[p].melds
                            result = self.hand_calculator.estimate_hand_value(
                                tiles,
                                win_tile,
                                melds=melds,
                                dora_indicators=dora_indicators,
                                config=HandConfig(is_tsumo=is_tsumo,
                                                  is_riichi=is_riichi,
                                                  player_wind=player_wind,
                                                  round_wind=round_wind,
                                                  is_rinshan=is_rinshan))
                            logger.info(result)
                            break
                        # if not win, we do the followings
                        if not self.table.players[0].in_riichi:
                            # choose a discard tile for playe `p`
                            discarded_tile = self.discard_tile(0)
                            # see if we can call riichi
                            if self.table.players[0].can_call_riichi():
                                logger.info("player {} call riichi.".format(0))
                                self.table.players[0].in_riichi = True
                        else:  # if riichi, we have to discard whatever we draw
                            discarded_tile = drawn_tile
                        # remove the tile from player's hand
                        self.table.players[0].tiles.remove(discarded_tile)
                        logger.debug("player {} discards {} after kan".format(
                            0, discarded_tile))
                        logger.debug("\tclosed hand: %s" %
                                     self.table.players[0].closed_hand)
                        logger.debug("\topen hand: %s" %
                                     self.table.players[0].open_hand_34_tiles)
                        logger.debug("\tmeld tiles: %s" %
                                     self.table.players[0].meld_tiles)
                        # we had to add it to discards, to calculate remaining tiles correctly
                        # drawn tile is not the one drawn from rinshan, but
                        # the one previously discarded by player `p`
                        self.table.add_discarded_tile(0, discarded_tile, True,
                                                      previous_drawn_tile)
                        # after program discarding a card, next player is 1
                        p = 1
                        continue
                    else:  # ryuukyoku
                        logger.debug("Rinshan empty. Ryuukyoku!")
                        break
                elif meld:  # pon, chi
                    logger.debug("player 0 %s from %d: %s" %
                                 (meld.type, p, meld))
                    player_seat = 0
                    # DEBUG: we change the add_called_meld method, delete the
                    # part that changes self.table.count_of_remaining_tiles
                    self.table.add_called_meld(player_seat, meld)
                    # Equivalently, program draws the tile discarded by opponent
                    self.table.players[0].draw_tile(tile)
                    # check p win
                    if self.check_win(0):
                        logger.info("player {} wins (by {})!".format(
                            0, meld.type))
                        tiles = self.table.players[p].tiles
                        win_tile = tile
                        (is_tsumo, is_riichi, is_dealer, open_sets,
                         dora_indicators, player_wind,
                         round_wind) = self.check_status(p)
                        ## TODO: Wait to be finished!
                        result = self.finished_hand.estimate_hand_value(
                            tiles,
                            win_tile,
                            is_tsumo=is_tsumo,
                            is_riichi=is_riichi,
                            is_dealer=is_dealer,
                            is_ippatsu=False,
                            is_rinshan=False,
                            is_chankan=False,
                            is_haitei=False,
                            is_houtei=False,
                            is_daburu_riichi=False,
                            is_nagashi_mangan=False,
                            is_tenhou=False,
                            is_renhou=False,
                            is_chiihou=False,
                            open_sets=open_sets,
                            dora_indicators=dora_indicators,
                            called_kan_indices=None,
                            player_wind=player_wind,
                            round_wind=round_wind)
                        logger.info(result)

                        melds = self.table.players[p].melds
                        result = self.hand_calculator.estimate_hand_value(
                            tiles,
                            win_tile,
                            melds=melds,
                            dora_indicators=dora_indicators,
                            config=HandConfig(is_tsumo=is_tsumo,
                                              is_riichi=is_riichi,
                                              player_wind=player_wind,
                                              round_wind=round_wind))
                        logger.info(result)
                        break
                    # if not win, we do the followings
                    if not self.table.players[0].in_riichi:
                        # choose a discard tile for playe `p`
                        discarded_tile = self.discard_tile(0)
                        # see if we can call riichi
                        if self.table.players[0].can_call_riichi():
                            logger.debug("player {} call riichi.".format(0))
                            self.table.players[0].in_riichi = True
                    else:  # if riichi, we can not call meld
                        raise ("Riichi player can not call meld!")
                    # remove the tile from player's hand
                    self.table.players[0].tiles.remove(discarded_tile)
                    # discarded tile added to table
                    self.table.add_discarded_tile(0, discarded_tile, True,
                                                  previous_drawn_tile)
                    logger.debug("player {} discards {} after {}".format(
                        0, discarded_tile, meld.type))
                    logger.debug("\tclosed hand: %s" %
                                 self.table.players[0].closed_hand)
                    logger.debug("\topen hand: %s" %
                                 self.table.players[0].open_hand_34_tiles)
                    logger.debug("\tmeld tiles: %s" %
                                 self.table.players[0].meld_tiles)
                    # after program discarding a card, next player is 1
                    p = 1
                    continue
            # we had to add it to discards, to calculate remaining tiles correctly
            self.table.add_discarded_tile(p, discarded_tile_tmp, is_tsumogiri,
                                          drawn_tile_tmp)

            # next player
            p = (p + 1) % 4

            logger.debug("[after]:%s,%s" %
                         (self.table.count_of_remaining_tiles,
                          len(self.table.remaining_tiles)))

        # output results
        logger.debug('\n')
        for p in range(4):
            logger.info("\tPlayer %d: %s (%s)" %
                        (p,
                         TilesConverter.to_one_line_string(
                             self.table.players[p].tiles),
                         TilesConverter.to_one_line_string(
                             self.table.players[p].closed_hand)))

    ## TODO: finish this part!
    def check_status(self, p, is_tsumo=False):
        """ We want to check the status of player `p`
        :param is_tsumo:
        :param is_riichi:
        :param is_dealer:
        :param is_ippatsu:
        :param is_rinshan:
        :param is_chankan:
        :param is_haitei:
        :param is_houtei:
        :param is_tenhou:
        :param is_renhou:
        :param is_chiihou:
        :param is_daburu_riichi:
        :param is_nagashi_mangan:
        :param open_sets: array of array with open sets in 34-tile format
        :param dora_indicators: array of tiles in 136-tile format
        :param called_kan_indices: array of tiles in 136-tile format
        :param player_wind: index of player wind
        :param round_wind: index of round wind
        """
        is_tsumo = is_tsumo
        is_riichi = self.table.players[p].in_riichi
        is_dealer = (self.table.dealer_seat == p)
        open_sets = [
            meld.tiles for meld in self.table.players[p].melds
            if meld.opened == True
        ]
        open_sets = list(map(lambda lst: [l // 4 for l in lst],
                             open_sets))  # convert to 34 format
        dora_indicators = self.table.dora_indicators
        player_wind = self.table.players[p].player_wind
        round_wind = self.table.round_wind

        return (is_tsumo, is_riichi, is_dealer, open_sets, dora_indicators,
                player_wind, round_wind)

    def _check2(self):
        self.sim_game()

    def _check(self):
        BW_riichi, BW_stealing, R, Wpet_riichi, Wpet_stealing = self.get_WPET(
            500)
        print("BW riichi: %s" % BW_riichi)
        print("BW stealing: %s" % BW_stealing)
        print("R: %s" % R)
        print("WPET riichi: %s" % Wpet_riichi)
        print("WPET stealing: %s" % Wpet_stealing)

        import matplotlib.pyplot as plt
        plt.plot(range(19), Wpet_riichi, '*-r', range(19), Wpet_stealing,
                 'o-b')
    def test_calculate_scores_and_ron_by_dealer(self):
        hand = FinishedHand()

        result = hand.calculate_scores(han=1,
                                       fu=30,
                                       is_tsumo=False,
                                       is_dealer=True)
        self.assertEqual(result['main'], 1500)

        result = hand.calculate_scores(han=2,
                                       fu=30,
                                       is_tsumo=False,
                                       is_dealer=True)
        self.assertEqual(result['main'], 2900)

        result = hand.calculate_scores(han=3,
                                       fu=30,
                                       is_tsumo=False,
                                       is_dealer=True)
        self.assertEqual(result['main'], 5800)

        result = hand.calculate_scores(han=4,
                                       fu=30,
                                       is_tsumo=False,
                                       is_dealer=True)
        self.assertEqual(result['main'], 11600)

        result = hand.calculate_scores(han=5,
                                       fu=0,
                                       is_tsumo=False,
                                       is_dealer=True)
        self.assertEqual(result['main'], 12000)

        result = hand.calculate_scores(han=6,
                                       fu=0,
                                       is_tsumo=False,
                                       is_dealer=True)
        self.assertEqual(result['main'], 18000)

        result = hand.calculate_scores(han=8,
                                       fu=0,
                                       is_tsumo=False,
                                       is_dealer=True)
        self.assertEqual(result['main'], 24000)

        result = hand.calculate_scores(han=11,
                                       fu=0,
                                       is_tsumo=False,
                                       is_dealer=True)
        self.assertEqual(result['main'], 36000)

        result = hand.calculate_scores(han=13,
                                       fu=0,
                                       is_tsumo=False,
                                       is_dealer=True)
        self.assertEqual(result['main'], 48000)

        result = hand.calculate_scores(han=26,
                                       fu=0,
                                       is_tsumo=False,
                                       is_dealer=True)
        self.assertEqual(result['main'], 96000)
    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))
Exemple #21
0
class MainAI(BaseAI):
    version = '0.2.7'

    agari = None
    shanten = None
    defence = None
    hand_divider = None
    finished_hand = None

    previous_shanten = 7
    in_defence = False
    waiting = None

    current_strategy = None

    def __init__(self, player):
        super(MainAI, self).__init__(player)

        self.agari = Agari()
        self.shanten = Shanten()
        self.defence = DefenceHandler(player)
        self.hand_divider = HandDivider()
        self.finished_hand = FinishedHand()
        self.previous_shanten = 7
        self.current_strategy = None
        self.waiting = []
        self.in_defence = False

    def erase_state(self):
        self.current_strategy = None
        self.in_defence = False

    def discard_tile(self):
        results, shanten = self.calculate_outs(self.player.tiles,
                                               self.player.closed_hand,
                                               self.player.open_hand_34_tiles)

        selected_tile = self.process_discard_options_and_select_tile_to_discard(
            results, shanten)

        # bot think that there is a threat on the table
        # and better to fold
        # if we can't find safe tiles, let's continue to build our hand
        if self.defence.should_go_to_defence_mode(selected_tile):
            if not self.in_defence:
                logger.info('We decided to fold against other players')
                self.in_defence = True

            defence_tile = self.defence.try_to_find_safe_tile_to_discard(
                results)
            if defence_tile:
                return self.process_discard_option(defence_tile,
                                                   self.player.closed_hand)
        else:
            self.in_defence = False

        return self.process_discard_option(selected_tile,
                                           self.player.closed_hand)

    def process_discard_options_and_select_tile_to_discard(
            self, results, shanten):
        tiles_34 = TilesConverter.to_34_array(self.player.tiles)

        # we had to update tiles value there
        # because it is related with shanten number
        for result in results:
            result.tiles_count = self.count_tiles(result.waiting, tiles_34)
            result.calculate_value(shanten)

        # current strategy can affect on our discard options
        # so, don't use strategy specific choices for calling riichi
        if self.current_strategy:
            results = self.current_strategy.determine_what_to_discard(
                self.player.closed_hand, results, shanten, False, None)

        return self.chose_tile_to_discard(results)

    def calculate_outs(self, tiles, closed_hand, open_sets_34=None):
        """
        :param tiles: array of tiles in 136 format
        :param closed_hand: array of tiles in 136 format
        :param open_sets_34: array of array with tiles in 34 format
        :return:
        """
        tiles_34 = TilesConverter.to_34_array(tiles)
        closed_tiles_34 = TilesConverter.to_34_array(closed_hand)
        is_agari = self.agari.is_agari(tiles_34,
                                       self.player.open_hand_34_tiles)

        results = []
        for hand_tile in range(0, 34):
            if not closed_tiles_34[hand_tile]:
                continue

            tiles_34[hand_tile] -= 1

            shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34)

            waiting = []
            for j in range(0, 34):
                if hand_tile == j or tiles_34[j] == 4:
                    continue

                tiles_34[j] += 1
                if self.shanten.calculate_shanten(tiles_34,
                                                  open_sets_34) == shanten - 1:
                    waiting.append(j)
                tiles_34[j] -= 1

            tiles_34[hand_tile] += 1

            if waiting:
                results.append(
                    DiscardOption(player=self.player,
                                  shanten=shanten,
                                  tile_to_discard=hand_tile,
                                  waiting=waiting,
                                  tiles_count=self.count_tiles(
                                      waiting, tiles_34)))

        if is_agari:
            shanten = Shanten.AGARI_STATE
        else:
            shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34)

        return results, shanten

    def count_tiles(self, waiting, tiles_34):
        n = 0
        for item in waiting:
            n += 4 - self.player.total_tiles(item, tiles_34)
        return n

    def try_to_call_meld(self, tile, is_kamicha_discard):
        if not self.current_strategy:
            return None, None

        return self.current_strategy.try_to_call_meld(tile, is_kamicha_discard)

    def determine_strategy(self):
        # for already opened hand we don't need to give up on selected strategy
        if self.player.is_open_hand and self.current_strategy:
            return False

        old_strategy = self.current_strategy
        self.current_strategy = None

        # order is important
        strategies = [
            YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player),
            HonitsuStrategy(BaseStrategy.HONITSU, self.player),
        ]

        if settings.OPEN_TANYAO:
            strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player))

        for strategy in strategies:
            if strategy.should_activate_strategy():
                self.current_strategy = strategy

        if self.current_strategy:
            if not old_strategy or self.current_strategy.type != old_strategy.type:
                message = '{} switched to {} strategy'.format(
                    self.player.name, self.current_strategy)
                if old_strategy:
                    message += ' from {}'.format(old_strategy)
                logger.debug(message)
                logger.debug('With hand: {}'.format(
                    TilesConverter.to_one_line_string(self.player.tiles)))

        if not self.current_strategy and old_strategy:
            logger.debug('{} gave up on {}'.format(self.player.name,
                                                   old_strategy))

        return self.current_strategy and True or False

    def chose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption:
        """
        Try to find best tile to discard, based on different valuations
        """
        def sorting(x):
            # - is important for x.tiles_count
            # in that case we will discard tile that will give for us more tiles
            # to complete a hand
            return x.shanten, -x.tiles_count, x.valuation

        had_to_be_discarded_tiles = [
            x for x in results if x.had_to_be_discarded
        ]
        if had_to_be_discarded_tiles:
            had_to_be_discarded_tiles = sorted(had_to_be_discarded_tiles,
                                               key=sorting)
            selected_tile = had_to_be_discarded_tiles[0]
        else:
            results = sorted(results, key=sorting)
            # remove needed tiles from discard options
            results = [x for x in results if not x.had_to_be_saved]

            # let's chose most valuable tile first
            temp_tile = results[0]
            # and let's find all tiles with same shanten
            results_with_same_shanten = [
                x for x in results if x.shanten == temp_tile.shanten
            ]
            possible_options = [temp_tile]
            for discard_option in results_with_same_shanten:
                # there is no sense to check already chosen tile
                if discard_option.tile_to_discard == temp_tile.tile_to_discard:
                    continue

                # we don't need to select tiles almost dead waits
                if discard_option.tiles_count <= 2:
                    continue

                # let's check all other tiles with same shanten
                # maybe we can find tiles that have almost same tiles count number
                if temp_tile.tiles_count - 2 < discard_option.tiles_count < temp_tile.tiles_count + 2:
                    possible_options.append(discard_option)

            # let's sort got tiles by value and let's chose less valuable tile to discard
            possible_options = sorted(possible_options,
                                      key=lambda x: x.valuation)
            selected_tile = possible_options[0]

        return selected_tile

    def process_discard_option(self, selected_tile, closed_hand):
        self.waiting = selected_tile.waiting
        self.player.ai.previous_shanten = selected_tile.shanten
        self.player.in_tempai = self.player.ai.previous_shanten == 0
        return selected_tile.find_tile_in_hand(closed_hand)

    def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False):
        """
        :param win_tile: 34 tile format
        :param tiles:
        :param call_riichi:
        :return:
        """
        win_tile *= 4
        # we don't need to think, that our waiting is aka dora
        if win_tile in AKA_DORA_LIST:
            win_tile += 1

        if not tiles:
            tiles = self.player.tiles

        tiles += [win_tile]
        result = self.finished_hand.estimate_hand_value(
            tiles=tiles,
            win_tile=win_tile,
            is_tsumo=False,
            is_riichi=call_riichi,
            is_dealer=self.player.is_dealer,
            open_sets=self.player.open_hand_34_tiles,
            player_wind=self.player.player_wind,
            round_wind=self.player.table.round_wind,
            dora_indicators=self.player.table.dora_indicators)
        return result

    def should_call_riichi(self):
        # empty waiting can be found in some cases
        if not self.waiting:
            return False

        # we have a good wait, let's riichi
        if len(self.waiting) > 1:
            return True

        waiting = self.waiting[0]
        tiles = self.player.closed_hand + [waiting * 4]
        closed_melds = [x for x in self.player.melds if not x.opened]
        for meld in closed_melds:
            tiles.extend(meld.tiles[:3])

        tiles_34 = TilesConverter.to_34_array(tiles)

        results = self.hand_divider.divide_hand(tiles_34, [], [])
        result = results[0]

        count_of_pairs = len([x for x in result if is_pair(x)])
        # with chitoitsu we can call a riichi with pair wait
        if count_of_pairs == 7:
            return True

        for hand_set in result:
            # better to not call a riichi for a pair wait
            # it can be easily improved
            if is_pair(hand_set) and waiting in hand_set:
                return False

        return True

    def can_call_kan(self, tile, open_kan):
        """
        Method will decide should we call a kan,
        or upgrade pon to kan
        :param tile: 136 tile format
        :param open_kan: boolean
        :return: kan type
        """
        # we don't need to add dora for other players
        if self.player.ai.in_defence:
            return None

        if open_kan:
            # we don't want to start open our hand from called kan
            if not self.player.is_open_hand:
                return None

            # there is no sense to call open kan when we are not in tempai
            if not self.player.in_tempai:
                return None

            # we have a bad wait, rinshan chance is low
            if len(self.waiting) < 2:
                return None

        tile_34 = tile // 4
        tiles_34 = TilesConverter.to_34_array(self.player.tiles)
        closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)
        pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)]

        # let's check can we upgrade opened pon to the kan
        if pon_melds:
            for meld in pon_melds:
                # tile is equal to our already opened pon,
                # so let's call chankan!
                if tile_34 in meld:
                    return Meld.CHANKAN

        count_of_needed_tiles = 4
        # for open kan 3 tiles is enough to call a kan
        if open_kan:
            count_of_needed_tiles = 3

        # we have 3 tiles in our hand,
        # so we can try to call closed meld
        if closed_hand_34[tile_34] == count_of_needed_tiles:
            if not open_kan:
                # to correctly count shanten in the hand
                # we had do subtract drown tile
                tiles_34[tile_34] -= 1

            melds = self.player.open_hand_34_tiles
            previous_shanten = self.shanten.calculate_shanten(tiles_34, melds)

            melds += [[tile_34, tile_34, tile_34]]
            new_shanten = self.shanten.calculate_shanten(tiles_34, melds)

            # called kan will not ruin our hand
            if new_shanten <= previous_shanten:
                return Meld.KAN

        return None

    @property
    def valued_honors(self):
        return [
            CHUN, HAKU, HATSU, self.player.table.round_wind,
            self.player.player_wind
        ]