Ejemplo n.º 1
0
class ImplementationAI(InterfaceAI):
    """
    AI that will discard tiles to maximize expected shanten.
    Assumes that tiles are drawn randomly from those not on the table or in hand - aka not revealed to player.
    Does not account for hidden tiles in opponent's hands.
    Always calls wins, never calls riichi.
    TODO: Everything
    """
    version = 'shantenMCS'

    shanten = None
    agari = None
    shdict = {}

    def __init__(self, player):
        super(ImplementationAI, self).__init__(player)
        self.shanten = Shanten()
        self.hand_divider = HandDivider()
        self.agari = Agari()
        self.iterations = 200

    def simulate_single(self, hand, hand_open, unaccounted_tiles):
        """
        #simulates a single random draw and calculates shanten

        hand, hand_open -- hand in 34 format
        unaccounted_tiles -- all the unused tiles in 34 format
        turn -- a number from 0-3 (0 is the player)
        """

        hand = list(hand)
        unaccounted = list(unaccounted_tiles)
        #14 in dead wall 13*3= 39 in other hand -> total 53
        unaccounted_nonzero = np.nonzero(unaccounted)[0]  #get a random card
        draw_tile = random.choice(unaccounted_nonzero)
        unaccounted[draw_tile] -= 1
        hand[draw_tile] += 1
        return self.shanten.calculate_shanten(hand, hand_open)

    # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables
    def discard_tile(self, discard_tile):

        if discard_tile is not None:
            return discard_tile

        tiles_34 = TilesConverter.to_34_array(self.player.tiles)
        closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand)

        results = []

        for tile in range(0, 34):
            # Can the tile be discarded from the concealed hand?
            if not closed_tiles_34[tile]:
                continue

            # discard the tile from hand
            tiles_34[tile] -= 1

            # calculate shanten and store
            shanten = self.shanten.calculate_shanten(
                tiles_34, self.player.open_hand_34_tiles)
            results.append((shanten, tile))

            # return tile to hand
            tiles_34[tile] += 1

        (minshanten, discard_34) = min(results)

        results2 = []
        unaccounted = (np.array([4]*34) - closed_tiles_34)\
            - TilesConverter.to_34_array(self.table.revealed_tiles)

        self.shdict = {}
        for shanten, tile in results:
            if shanten != minshanten:
                continue
            h = sum(
                self.simulate_single(closed_tiles_34, self.player.
                                     open_hand_34_tiles, unaccounted)
                for _ in range(self.iterations)) / self.iterations
            results2.append((h, tile))

        (h, discard_34) = min(results2)

        discard_136 = TilesConverter.find_34_tile_in_136_array(
            discard_34, self.player.closed_hand)

        if discard_136 is None:
            logger.debug('Failure')
            discard_136 = random.randrange(len(self.player.tiles) - 1)
            discard_136 = self.player.tiles[discard_136]
        logger.info('Shanten after discard:' + str(shanten))
        logger.info('Discard heuristic:' + str(h))
        return discard_136

    # UNUSED
    def calculate_outs(self, discard_34, shanten, depth=2):
        closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand)
        table_34 = list(self.table.revealed_tiles)
        tiles_34 = TilesConverter.to_34_array(self.player.tiles)
        table_34[discard_34] += 1
        closed_tiles_34[discard_34] -= 1
        tiles_34[discard_34] -= 1

        hidden_34 = np.array(
            [4] * 34) - np.array(closed_tiles_34) - np.array(table_34)

        # print(hidden_34)

        # want to sample? use this
        # reveal_num = sum(hidden_34)
        # draw_p = [float(i)/reveal_num for i in hidden_34]
        # draw = np.random.choice(34, p=draw_p)

        return self.out_search(tiles_34, closed_tiles_34, hidden_34, depth,
                               shanten - 1)

    def calculate_outs_meld(self,
                            discard_34,
                            shanten,
                            tiles_34,
                            closed_tiles_34,
                            open_hand_34,
                            depth=2):
        table_34 = list(self.table.revealed_tiles)
        tiles_34 = copy.deepcopy(tiles_34)
        closed_tiles_34 = copy.deepcopy(closed_tiles_34)
        table_34[discard_34] += 1
        closed_tiles_34[discard_34] -= 1
        tiles_34[discard_34] -= 1

        hidden_34 = np.array(
            [4] * 34) - np.array(closed_tiles_34) - np.array(table_34)

        return self.out_search(tiles_34, closed_tiles_34, hidden_34, depth,
                               shanten - 1, open_hand_34)

    def out_search(self,
                   tiles_34,
                   closed_tiles_34,
                   hidden_34,
                   depth,
                   shanten,
                   open_hand_34=None):
        outs = 0
        for i in range(34):
            if hidden_34[i] <= 0:
                continue
            ct = hidden_34[i]

            # draw tile from hidden to concealed hand
            hidden_34[i] -= 1
            closed_tiles_34[i] += 1
            tiles_34[i] += 1

            if self.agari.is_agari(tiles_34, open_hand_34):
                outs += 2
            else:
                for tile in range(0, 34):
                    # Can the tile be discarded from the concealed hand?
                    if not closed_tiles_34[tile]:
                        continue

                    # discard the tile from hand
                    closed_tiles_34[tile] -= 1
                    tiles_34[tile] -= 1

                    tuple_34 = tuple(tiles_34)
                    # calculate shanten and add outs if appropriate
                    if tuple_34 in self.shdict.keys():
                        sh = self.shdict[tuple_34]
                    else:
                        if open_hand_34 is None:
                            sh = self.shanten.calculate_shanten(
                                tiles_34, self.player.open_hand_34_tiles)
                        else:
                            sh = self.shanten.calculate_shanten(
                                tiles_34, open_hand_34)
                        self.shdict[tuple_34] = sh
                    if sh == shanten:
                        if depth <= 1 or shanten == -1:
                            outs += 1
                        else:
                            outs += ct * self.out_search(
                                tiles_34, closed_tiles_34, hidden_34,
                                depth - 1, shanten - 1)
                    if sh == shanten + 1:
                        outs += 0.01

                    # return tile to hand
                    closed_tiles_34[tile] += 1
                    tiles_34[tile] += 1

            # return tile from closed hand to hidden
            hidden_34[i] += 1
            closed_tiles_34[i] -= 1
            tiles_34[i] -= 1

        return outs

    def should_call_riichi(self):
        # if len(self.player.open_hand_34_tiles) != 0:
        #     return False
        return True
        # tiles_34 = TilesConverter.to_34_array(self.player.tiles)
        # shanten = self.shanten.calculate_shanten(tiles_34, None)
        # logger.debug('Riichi check, shanten = ' + str(shanten))
        # return shanten == 0

    def should_call_win(self, tile, enemy_seat):
        return True

    def should_call_kan(self, tile, open_kan):
        """
        When bot can call kan or chankan this method will be called
        :param tile: 136 tile format
        :param is_open_kan: boolean
        :return: kan type (Meld.KAN, Meld.CHANKAN) or None
        """

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

            # don't call open kan if not waiting for win
            if not self.player.in_tempai:
                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)]

        # upgrade open pon to kan if possible
        if pon_melds:
            for meld in pon_melds:
                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

        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)

            # check for improvement in shanten
            if new_shanten <= previous_shanten:
                return Meld.KAN

        return None

    def try_to_call_meld(self, tile, is_kamicha_discard):
        """
        When bot can open hand with a set (chi or pon/kan) this method will be called
        :param tile: 136 format tile
        :param is_kamicha_discard: boolean
        :return: Meld and DiscardOption objects or None, None
        """

        # can't call if in riichi
        if self.player.in_riichi:
            return None, None

        closed_hand = self.player.closed_hand[:]

        # check for appropriate hand size, seems to solve a bug
        if len(closed_hand) == 1:
            return None, None

        # get old shanten value
        old_tiles_34 = TilesConverter.to_34_array(self.player.tiles)
        old_shanten = self.shanten.calculate_shanten(
            old_tiles_34, self.player.open_hand_34_tiles)

        # setup
        discarded_tile = tile // 4
        new_closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile])

        # We will use hand_divider to find possible melds involving the discarded tile.
        # Check its suit and number to narrow the search conditions
        # skipping this will break the default mahjong functions
        combinations = []
        first_index = 0
        second_index = 0
        if is_man(discarded_tile):
            first_index = 0
            second_index = 8
        elif is_pin(discarded_tile):
            first_index = 9
            second_index = 17
        elif is_sou(discarded_tile):
            first_index = 18
            second_index = 26

        if second_index == 0:
            # honor tiles
            if new_closed_hand_34[discarded_tile] == 3:
                combinations = [[[discarded_tile] * 3]]
        else:
            # to avoid not necessary calculations
            # we can check only tiles around +-2 discarded tile
            first_limit = discarded_tile - 2
            if first_limit < first_index:
                first_limit = first_index

            second_limit = discarded_tile + 2
            if second_limit > second_index:
                second_limit = second_index

            combinations = self.hand_divider.find_valid_combinations(
                new_closed_hand_34, first_limit, second_limit, True)
        # Reduce combinations to list of melds
        if combinations:
            combinations = combinations[0]

        # Verify that a meld can be called
        possible_melds = []
        for meld_34 in combinations:
            # we can call pon from everyone
            if is_pon(meld_34) and discarded_tile in meld_34:
                if meld_34 not in possible_melds:
                    possible_melds.append(meld_34)

            # we can call chi only from left player
            if is_chi(meld_34
                      ) and is_kamicha_discard and discarded_tile in meld_34:
                if meld_34 not in possible_melds:
                    possible_melds.append(meld_34)

        # For each possible meld, check if calling it and discarding can improve shanten
        new_shanten = float('inf')
        discard_136 = None
        tiles = None

        for meld_34 in possible_melds:
            shanten, disc = self.meldDiscard(meld_34, tile)
            if shanten < new_shanten:
                new_shanten, discard_136 = shanten, disc
                tiles = meld_34

        # If shanten can be improved by calling meld, call it
        if new_shanten < old_shanten:
            meld = Meld()
            meld.type = is_chi(tiles) and Meld.CHI or Meld.PON

            # convert meld tiles back to 136 format for Meld type return
            # find them in a copy of the closed hand and remove
            tiles.remove(discarded_tile)

            first_tile = TilesConverter.find_34_tile_in_136_array(
                tiles[0], closed_hand)
            closed_hand.remove(first_tile)

            second_tile = TilesConverter.find_34_tile_in_136_array(
                tiles[1], closed_hand)
            closed_hand.remove(second_tile)

            tiles_136 = [first_tile, second_tile, tile]

            discard_136 = TilesConverter.find_34_tile_in_136_array(
                discard_136 // 4, closed_hand)
            meld.tiles = sorted(tiles_136)
            return meld, discard_136

        return None, None

    # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables
    def meldDiscard(self, meld_34, discardtile):

        tiles_34 = TilesConverter.to_34_array(self.player.tiles +
                                              [discardtile])
        closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand +
                                                     [discardtile])
        open_hand_34 = copy.deepcopy(self.player.open_hand_34_tiles)

        # remove meld from closed and and add to open hand
        open_hand_34.append(meld_34)
        for tile_34 in meld_34:
            closed_tiles_34[tile_34] -= 1

        results = []

        for tile in range(0, 34):

            # Can the tile be discarded from the concealed hand?
            if not closed_tiles_34[tile]:
                continue

            # discard the tile from hand
            tiles_34[tile] -= 1

            # calculate shanten and store
            shanten = self.shanten.calculate_shanten(tiles_34, open_hand_34)
            results.append((shanten, tile))

            # return tile to hand
            tiles_34[tile] += 1

        (minshanten, discard_34) = min(results)

        results2 = []
        unaccounted = (np.array([4]*34) - closed_tiles_34)\
            - TilesConverter.to_34_array(self.table.revealed_tiles)

        self.shdict = {}
        for shanten, tile in results:
            if shanten != minshanten:
                continue
            h = sum(
                self.simulate_single(closed_tiles_34, open_hand_34,
                                     unaccounted)
                for _ in range(self.iterations)) / self.iterations
            results2.append((h, tile))

        (h, discard_34) = min(results2)

        discard_136 = TilesConverter.find_34_tile_in_136_array(
            discard_34, self.player.closed_hand)

        return minshanten, discard_136
Ejemplo n.º 2
0
class ImplementationAI(InterfaceAI):
    """
    AI that will discard tiles as to minimize shanten, using perfect shanten calculation.
    Picks the first tile with resulting in the lowest shanten value when choosing what to discard.
    Calls riichi if possible and hand is closed.
    Always calls wins.
    Calls kan to upgrade pon or on equivalent or reduced shanten.
    Calls melds to reduce shanten.
    """
    version = 'shantenNaive'

    shanten = None
    agari = None

    def __init__(self, player):
        super(ImplementationAI, self).__init__(player)
        self.shanten = Shanten()
        # self.agari = Agari()
        self.hand_divider = HandDivider()

    # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables
    def discard_tile(self, discard_tile):

        if discard_tile is not None:
            return discard_tile

        tiles_34 = TilesConverter.to_34_array(self.player.tiles)
        closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand)
        # is_agari = self.agari.is_agari(tiles_34, self.player.open_hand_34_tiles)

        results = []

        for tile in range(0,34):
            # Can the tile be discarded from the concealed hand?
            if not closed_tiles_34[tile]:
                continue

            # discard the tile from hand
            tiles_34[tile] -= 1

            # calculate shanten and store
            shanten = self.shanten.calculate_shanten(tiles_34, self.player.open_hand_34_tiles)
            results.append((shanten, tile))

            # return tile to hand
            tiles_34[tile] += 1

        (shanten, discard_34) = min(results)

        discard_136 = TilesConverter.find_34_tile_in_136_array(discard_34, self.player.closed_hand)

        if discard_136 is None:
            logger.debug('Greedy search or tile conversion failed')
            discard_136 = random.randrange(len(self.player.tiles) - 1)
            discard_136 = self.player.tiles[discard_136]
        logger.info('Shanten after discard:' + str(shanten))
        return discard_136

    def should_call_riichi(self):
        return True

    def should_call_win(self, tile, enemy_seat):
        return True

    def should_call_kan(self, tile, open_kan):
        """
        When bot can call kan or chankan this method will be called
        :param tile: 136 tile format
        :param is_open_kan: boolean
        :return: kan type (Meld.KAN, Meld.CHANKAN) or None
        """

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

            # don't call open kan if not waiting for win
            if not self.player.in_tempai:
                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)]

        # upgrade open pon to kan if possible
        if pon_melds:
            for meld in pon_melds:
                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

        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)

            # check for improvement in shanten
            if new_shanten <= previous_shanten:
                return Meld.KAN

        return None

    def try_to_call_meld(self, tile, is_kamicha_discard):
        """
        When bot can open hand with a set (chi or pon/kan) this method will be called
        :param tile: 136 format tile
        :param is_kamicha_discard: boolean
        :return: Meld and DiscardOption objects or None, None
        """

        # can't call if in riichi
        if self.player.in_riichi:
            return None, None

        closed_hand = self.player.closed_hand[:]

        # check for appropriate hand size, seems to solve a bug
        if len(closed_hand) == 1:
            return None, None

        # get old shanten value
        old_tiles_34 = TilesConverter.to_34_array(self.player.tiles)
        old_shanten = self.shanten.calculate_shanten(old_tiles_34, self.player.open_hand_34_tiles)

        # setup
        discarded_tile = tile // 4
        new_closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile])

        # We will use hand_divider to find possible melds involving the discarded tile.
        # Check its suit and number to narrow the search conditions
        # skipping this will break the default mahjong functions
        combinations = []
        first_index = 0
        second_index = 0
        if is_man(discarded_tile):
            first_index = 0
            second_index = 8
        elif is_pin(discarded_tile):
            first_index = 9
            second_index = 17
        elif is_sou(discarded_tile):
            first_index = 18
            second_index = 26

        if second_index == 0:
            # honor tiles
            if new_closed_hand_34[discarded_tile] == 3:
                combinations = [[[discarded_tile] * 3]]
        else:
            # to avoid not necessary calculations
            # we can check only tiles around +-2 discarded tile
            first_limit = discarded_tile - 2
            if first_limit < first_index:
                first_limit = first_index

            second_limit = discarded_tile + 2
            if second_limit > second_index:
                second_limit = second_index

            combinations = self.hand_divider.find_valid_combinations(new_closed_hand_34,
                                                                           first_limit,
                                                                           second_limit, True)
        # Reduce combinations to list of melds
        if combinations:
            combinations = combinations[0]

        # Verify that a meld can be called
        possible_melds = []
        for meld_34 in combinations:
            # we can call pon from everyone
            if is_pon(meld_34) and discarded_tile in meld_34:
                if meld_34 not in possible_melds:
                    possible_melds.append(meld_34)

            # we can call chi only from left player
            if is_chi(meld_34) and is_kamicha_discard and discarded_tile in meld_34:
                if meld_34 not in possible_melds:
                    possible_melds.append(meld_34)

        # For each possible meld, check if calling it and discarding can improve shanten
        new_shanten = float('inf')
        discard_136 = None
        tiles = None

        for meld_34 in possible_melds:
            shanten, disc = self.meldDiscard(meld_34, tile)
            if shanten < new_shanten:
                new_shanten, discard_136 = shanten, disc
                tiles = meld_34

        # If shanten can be improved by calling meld, call it
        if new_shanten < old_shanten:
            meld = Meld()
            meld.type = is_chi(tiles) and Meld.CHI or Meld.PON

            # convert meld tiles back to 136 format for Meld type return
            # find them in a copy of the closed hand and remove
            tiles.remove(discarded_tile)

            first_tile = TilesConverter.find_34_tile_in_136_array(tiles[0], closed_hand)
            closed_hand.remove(first_tile)

            second_tile = TilesConverter.find_34_tile_in_136_array(tiles[1], closed_hand)
            closed_hand.remove(second_tile)

            tiles_136 = [
                first_tile,
                second_tile,
                tile
            ]

            discard_136 = TilesConverter.find_34_tile_in_136_array(discard_136 // 4, closed_hand)
            meld.tiles = sorted(tiles_136)
            return meld, discard_136

        return None, None

    # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables
    def meldDiscard(self, meld_34, discardtile):

        tiles_34 = TilesConverter.to_34_array(self.player.tiles + [discardtile])
        closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand + [discardtile])
        open_hand_34 = copy.deepcopy(self.player.open_hand_34_tiles)

        # remove meld from closed and and add to open hand
        open_hand_34.append(meld_34)
        for tile_34 in meld_34:
            closed_tiles_34[tile_34] -= 1

        results = []

        for tile in range(0, 34):

            # Can the tile be discarded from the concealed hand?
            if not closed_tiles_34[tile]:
                continue

            # discard the tile from hand
            tiles_34[tile] -= 1

            # calculate shanten and store
            shanten = self.shanten.calculate_shanten(tiles_34, open_hand_34)
            results.append((shanten, tile))

            # return tile to hand
            tiles_34[tile] += 1

        (shanten, discard_34) = min(results)

        discard_136 = TilesConverter.find_34_tile_in_136_array(discard_34, self.player.closed_hand)

        return shanten, discard_136
Ejemplo n.º 3
0
class Phoenix:
    def __init__(self, player):
        self.player = player
        self.table = player.table

        self.chi = Chi(player)
        self.pon = Pon(player)
        self.kan = Kan(player)
        self.riichi = Riichi(player)
        self.discard = Discard(player)
        self.grp = GlobalRewardPredictor()
        self.hand_builder = HandBuilder(player, self)
        self.shanten_calculator = Shanten()
        self.hand_cache_shanten = {}
        self.placement = player.config.PLACEMENT_HANDLER_CLASS(player)
        self.finished_hand = HandCalculator()
        self.hand_divider = HandDivider()

        self.erase_state()

    def erase_state(self):
        self.hand_cache_shanten = {}
        self.hand_cache_estimation = {}
        self.finished_hand = HandCalculator()
        self.grp_features = []

    def collect_experience(self):

        #collect round info
        init_scores = np.array(self.table.init_scores) / 1e5
        gains = np.array(self.table.gains) / 1e5
        dans = np.array(
            [RANKS.index(p.rank) for p in self.player.table.players])
        dealer = int(self.player.dealer_seat)
        repeat_dealer = self.player.table.count_of_honba_sticks
        riichi_bets = self.player.table.count_of_riichi_sticks

        features = np.concatenate(
            (init_scores, gains, dans,
             np.array([dealer, repeat_dealer, riichi_bets])),
            axis=0)
        self.grp_features.append(features)

        #prepare input
        grp_input = [np.zeros(pred_emb_dim)] * max(
            round_num - len(self.grp_features), 0) + self.grp_features[:]

        reward = self.grp.get_global_reward(
            np.expand_dims(np.asarray(grp_input), axis=0))[0][self.player.seat]

        for model in [self.chi, self.pon, self.kan, self.riichi, self.discard]:
            model.collector.complete_episode(reward)

    def write_buffer(self):
        for model in [self.chi, self.pon, self.kan, self.riichi, self.discard]:
            model.collector.to_buffer()

    def init_hand(self):
        self.player.logger.debug(
            log.INIT_HAND,
            context=[
                f"Round  wind: {DISPLAY_WINDS[self.table.round_wind_tile]}",
                f"Player wind: {DISPLAY_WINDS[self.player.player_wind]}",
                f"Hand: {self.player.format_hand_for_print()}",
            ],
        )
        self.shanten, _ = self.hand_builder.calculate_shanten_and_decide_hand_structure(
            TilesConverter.to_34_array(self.player.tiles))

    def draw_tile(self):
        pass

    def discard_tile(self, discard_tile):
        '''
        return discarded_tile and with_riichi
        '''
        if discard_tile is not None:  #discard after meld
            return discard_tile, False
        if self.player.is_open_hand:  #can not riichi
            return self.discard.discard_tile(), False

        shanten = self.calculate_shanten_or_get_from_cache(
            TilesConverter.to_34_array(self.player.closed_hand))
        if shanten != 0:  #can not riichi
            return self.discard.discard_tile(), False
        with_riichi, p = self.riichi.should_call_riichi()
        if with_riichi:
            # fix here: might need review
            riichi_options = [
                tile for tile in self.player.closed_hand
                if self.calculate_shanten_or_get_from_cache(
                    TilesConverter.to_34_array(
                        [t for t in self.player.closed_hand
                         if t != tile])) == 0
            ]
            tile_to_discard = self.discard.discard_tile(
                with_riichi_options=riichi_options)
        else:
            tile_to_discard = self.discard.discard_tile()
        return tile_to_discard, with_riichi

    def try_to_call_meld(self, tile_136, is_kamicha_discard, meld_type):
        # 1 pon
        # 2 kan (it is a closed kan and can be send only to the self draw)
        # 4 chi
        # there is two return value, meldPrint() and discardOption(),
        # while the second would not be used by client.py
        meld_chi, meld_pon = None, None
        should_chi, should_pon = False, False

        # print(tile_136)
        # print(self.player.closed_hand)
        melds_chi, melds_pon = self.get_possible_meld(tile_136,
                                                      is_kamicha_discard)
        if melds_chi and meld_type & 4:
            should_chi, chi_score, tiles_chi = self.chi.should_call_chi(
                tile_136, melds_chi)
            # fix here: tiles_chi is now the first possible meld ---fixed!
            # tiles_chi = melds_chi[0]
            meld_chi = Meld(meld_type="chi",
                            tiles=tiles_chi) if meld_chi else None
        if melds_pon and meld_type & 1:
            should_pon, pon_score = self.pon.should_call_pon(
                tile_136, is_kamicha_discard)
            tiles_pon = melds_pon[0]
            meld_pon = Meld(meld_type="pon",
                            tiles=tiles_pon) if meld_pon else None

        if not should_chi and not should_pon:
            return None, None

        if should_chi and should_pon:
            meld = meld_chi if chi_score > pon_score else meld_pon
        elif should_chi:
            meld = meld_chi
        else:
            meld = meld_pon

        all_tiles_copy, meld_tiles_copy = self.player.tiles[:], self.player.meld_tiles[:]
        all_tiles_copy.append(tile_136)
        meld_tiles_copy.append(meld)
        closed_hand_copy = [
            item for item in all_tiles_copy if item not in meld_tiles_copy
        ]
        discard_option = self.discard.discard_tile(
            all_hands_136=all_tiles_copy, closed_hands_136=closed_hand_copy)

        return meld, discard_option

    def should_call_kyuushu_kyuuhai(self):
        #try kokushi strategy if with 10 types
        tiles_34 = TilesConverter.to_34_array(self.player.tiles)
        types = sum([1 for t in tiles_34 if t > 0])
        if types >= 10:
            return False
        else:
            return True

    def should_call_win(self,
                        tile,
                        is_tsumo,
                        enemy_seat=None,
                        is_chankan=False):
        # don't skip win in riichi
        if self.player.in_riichi:
            return True

        # currently we don't support win skipping for tsumo
        if is_tsumo:
            return True

        # fast path - check it first to not calculate hand cost
        cost_needed = self.placement.get_minimal_cost_needed()
        if cost_needed == 0:
            return True

        # 1 and not 0 because we call check for win this before updating remaining tiles
        is_hotei = self.player.table.count_of_remaining_tiles == 1

        hand_response = self.calculate_exact_hand_value_or_get_from_cache(
            tile,
            tiles=self.player.tiles,
            call_riichi=self.player.in_riichi,
            is_tsumo=is_tsumo,
            is_chankan=is_chankan,
            is_haitei=is_hotei,
        )
        assert hand_response is not None
        assert not hand_response.error, hand_response.error
        cost = hand_response.cost
        return self.placement.should_call_win(cost, is_tsumo, enemy_seat)

    def calculate_shanten_or_get_from_cache(self,
                                            closed_hand_34: List[int],
                                            use_chiitoitsu=True):
        """
        Sometimes we are calculating shanten for the same hand multiple times
        to save some resources let's cache previous calculations
        """
        key = build_shanten_cache_key(closed_hand_34, use_chiitoitsu)
        if key in self.hand_cache_shanten:
            return self.hand_cache_shanten[key]
        # if use_chiitoitsu and not self.player.is_open_hand:
        #     result = self.shanten_calculator.calculate_shanten_for_chiitoitsu_hand(closed_hand_34)
        # else:
        #     result = self.shanten_calculator.calculate_shanten_for_regular_hand(closed_hand_34)

        # fix here: a little bit strange in use_chiitoitsu
        shanten_results = []
        if use_chiitoitsu and not self.player.is_open_hand:
            shanten_results.append(
                self.shanten_calculator.calculate_shanten_for_chiitoitsu_hand(
                    closed_hand_34))
        shanten_results.append(
            self.shanten_calculator.calculate_shanten_for_regular_hand(
                closed_hand_34))
        result = min(shanten_results)
        self.hand_cache_shanten[key] = result
        return result

    def calculate_exact_hand_value_or_get_from_cache(
        self,
        win_tile_136,
        tiles=None,
        call_riichi=False,
        is_tsumo=False,
        is_chankan=False,
        is_haitei=False,
        is_ippatsu=False,
    ):
        if not tiles:
            tiles = self.player.tiles[:]
        else:
            tiles = tiles[:]
        if win_tile_136 not in tiles:
            tiles += [win_tile_136]

        additional_han = 0
        if is_chankan:
            additional_han += 1
        if is_haitei:
            additional_han += 1
        if is_ippatsu:
            additional_han += 1

        config = HandConfig(
            is_riichi=call_riichi,
            player_wind=self.player.player_wind,
            round_wind=self.player.table.round_wind_tile,
            is_tsumo=is_tsumo,
            options=OptionalRules(
                has_aka_dora=self.player.table.has_aka_dora,
                has_open_tanyao=self.player.table.has_open_tanyao,
                has_double_yakuman=False,
            ),
            is_chankan=is_chankan,
            is_ippatsu=is_ippatsu,
            is_haitei=is_tsumo and is_haitei or False,
            is_houtei=(not is_tsumo) and is_haitei or False,
            tsumi_number=self.player.table.count_of_honba_sticks,
            kyoutaku_number=self.player.table.count_of_riichi_sticks,
        )

        return self._estimate_hand_value_or_get_from_cache(
            win_tile_136, tiles, call_riichi, is_tsumo, additional_han, config)

    def _estimate_hand_value_or_get_from_cache(self,
                                               win_tile_136,
                                               tiles,
                                               call_riichi,
                                               is_tsumo,
                                               additional_han,
                                               config,
                                               is_rinshan=False,
                                               is_chankan=False):
        cache_key = build_estimate_hand_value_cache_key(
            tiles,
            call_riichi,
            is_tsumo,
            self.player.melds,
            self.player.table.dora_indicators,
            self.player.table.count_of_riichi_sticks,
            self.player.table.count_of_honba_sticks,
            additional_han,
            is_rinshan,
            is_chankan,
        )
        if self.hand_cache_estimation.get(cache_key):
            return self.hand_cache_estimation.get(cache_key)

        result = self.finished_hand.estimate_hand_value(
            tiles,
            win_tile_136,
            self.player.melds,
            self.player.table.dora_indicators,
            config,
            use_hand_divider_cache=True,
        )

        self.hand_cache_estimation[cache_key] = result
        return result

    @property
    def enemy_players(self):
        """
        Return list of players except our bot
        """
        return self.player.table.players[1:]

    def enemy_called_riichi(self, enemy_seat):
        """
        After enemy riichi we had to check will we fold or not
        it is affect open hand decisions
        :return:
        """
        pass

    def get_possible_meld(self, tile, is_kamicha_discard):

        closed_hand = self.player.closed_hand[:]

        # we can't open hand anymore
        if len(closed_hand) == 1:
            return None, None

        discarded_tile = tile // 4
        closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile])

        combinations = []
        first_index = 0
        second_index = 0
        if is_man(discarded_tile):
            first_index = 0
            second_index = 8
        elif is_pin(discarded_tile):
            first_index = 9
            second_index = 17
        elif is_sou(discarded_tile):
            first_index = 18
            second_index = 26

        if second_index == 0:
            # honor tiles
            if closed_hand_34[discarded_tile] == 3:
                combinations = [[[discarded_tile] * 3]]
        else:
            # to avoid not necessary calculations
            # we can check only tiles around +-2 discarded tile
            first_limit = discarded_tile - 2
            if first_limit < first_index:
                first_limit = first_index

            second_limit = discarded_tile + 2
            if second_limit > second_index:
                second_limit = second_index

            combinations = self.hand_divider.find_valid_combinations(
                closed_hand_34, first_limit, second_limit, True)

        if combinations:
            combinations = combinations[0]
        # possible_melds = []
        melds_chi, melds_pon = [], []
        for best_meld_34 in combinations:
            # we can call pon from everyone
            if is_pon(best_meld_34) and discarded_tile in best_meld_34:
                if best_meld_34 not in melds_pon:
                    melds_pon.append(best_meld_34)

            # we can call chi only from left player
            if is_chi(
                    best_meld_34
            ) and is_kamicha_discard and discarded_tile in best_meld_34:
                if best_meld_34 not in melds_chi:
                    melds_chi.append(best_meld_34)

        return melds_chi, melds_pon

    def enemy_called_riichi(self, enemy_seat):
        """
        After enemy riichi we had to check will we fold or not
        it is affect open hand decisions
        :return:
        """
        pass