Exemplo n.º 1
0
    def discard_tile(self, tiles, closed_hand, melds, print_log=True):
        selected_tile = self.choose_tile_to_discard(
            tiles,
            closed_hand,
            melds,
            print_log=print_log
        )

        # 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.ai.defence.should_go_to_defence_mode(selected_tile):
            if not self.ai.in_defence:
                DecisionsLogger.debug(log.DEFENCE_ACTIVATE)
                self.ai.in_defence = True

            defence_tile = self.ai.defence.try_to_find_safe_tile_to_discard()
            if defence_tile:
                return self.process_discard_option(defence_tile, closed_hand)
        else:
            if self.ai.in_defence:
                DecisionsLogger.debug(log.DEFENCE_DEACTIVATE)
            self.ai.in_defence = False

        return self.process_discard_option(selected_tile, closed_hand, print_log=print_log)
    def discard_tile(self, tiles, closed_hand, melds, print_log=True):
        selected_tile = self.choose_tile_to_discard(
            tiles,
            closed_hand,
            melds,
            print_log=print_log
        )

        # 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.ai.defence.should_go_to_defence_mode(selected_tile):
            if not self.ai.in_defence:
                DecisionsLogger.debug(log.DEFENCE_ACTIVATE)
                self.ai.in_defence = True

            defence_tile = self.ai.defence.try_to_find_safe_tile_to_discard()
            if defence_tile:
                return self.process_discard_option(defence_tile, closed_hand)
        else:
            if self.ai.in_defence:
                DecisionsLogger.debug(log.DEFENCE_DEACTIVATE)
            self.ai.in_defence = False

        return self.process_discard_option(selected_tile, closed_hand, print_log=print_log)
Exemplo n.º 3
0
    def try_to_call_meld(self, tile_136, is_kamicha_discard, remaining_tiles):
        tiles_136_previous = self.player.tiles[:]
        tiles_136 = tiles_136_previous + [tile_136]
        self.determine_strategy(tiles_136)

        if not self.current_strategy:
            return None, None

        tiles_34_previous = TilesConverter.to_34_array(tiles_136_previous)
        previous_shanten, _ = self.hand_builder.calculate_shanten(
            tiles_34_previous, self.player.meld_34_tiles)

        if previous_shanten == Shanten.AGARI_STATE and not self.current_strategy.can_meld_into_agari(
        ):
            return None, None

        meld, discard_option = self.current_strategy.try_to_call_meld(
            tile_136, is_kamicha_discard, tiles_136, remaining_tiles)
        if discard_option:
            self.last_discard_option = discard_option

            DecisionsLogger.debug(
                log.MELD_CALL,
                'Try to call meld',
                context=[
                    'Hand: {}'.format(
                        self.player.format_hand_for_print(tile_136)),
                    'Meld: {}'.format(meld),
                    'Discard after meld: {}'.format(discard_option)
                ])

        return meld, discard_option
Exemplo n.º 4
0
    def try_to_call_meld(self, tile_136, is_kamicha_discard):
        tiles_136_previous = self.player.tiles[:]
        tiles_136 = tiles_136_previous + [tile_136]
        self.determine_strategy(tiles_136)

        if not self.current_strategy:
            return None, None

        tiles_34_previous = TilesConverter.to_34_array(tiles_136_previous)
        previous_shanten, _ = self.hand_builder.calculate_shanten(tiles_34_previous, self.player.meld_34_tiles)

        if previous_shanten == Shanten.AGARI_STATE and not self.current_strategy.can_meld_into_agari():
            return None, None

        meld, discard_option = self.current_strategy.try_to_call_meld(tile_136, is_kamicha_discard, tiles_136)
        if discard_option:
            self.last_discard_option = discard_option

            DecisionsLogger.debug(log.MELD_CALL, 'Try to call meld', context=[
                'Hand: {}'.format(self.player.format_hand_for_print(tile_136)),
                'Meld: {}'.format(meld),
                'Discard after meld: {}'.format(discard_option)
            ])

        return meld, discard_option
Exemplo n.º 5
0
    def init_hand(self):
        DecisionsLogger.debug(log.INIT_HAND, context=[
            'Round  wind: {}'.format(DISPLAY_WINDS[self.table.round_wind_tile]),
            'Player wind: {}'.format(DISPLAY_WINDS[self.player.player_wind]),
            'Hand: {}'.format(self.player.format_hand_for_print()),
        ])

        self.shanten, _ = self.hand_builder.calculate_shanten(TilesConverter.to_34_array(self.player.tiles))
Exemplo n.º 6
0
    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:
        """

        if self.defence.should_go_to_defence_mode():
            self.in_defence = True
            DecisionsLogger.debug(log.DEFENCE_ACTIVATE)
Exemplo n.º 7
0
    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:
        """

        if self.defence.should_go_to_defence_mode():
            self.in_defence = True
            DecisionsLogger.debug(log.DEFENCE_ACTIVATE)
Exemplo n.º 8
0
    def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand,
                                discarded_tile):
        discarded_tile_34 = discarded_tile // 4

        final_results = []
        for meld_34 in possible_melds:
            meld_34_copy = meld_34.copy()
            closed_hand_copy = closed_hand.copy()

            meld_type = is_chi(meld_34_copy) and Meld.CHI or Meld.PON
            meld_34_copy.remove(discarded_tile_34)

            first_tile = TilesConverter.find_34_tile_in_136_array(
                meld_34_copy[0], closed_hand_copy)
            closed_hand_copy.remove(first_tile)

            second_tile = TilesConverter.find_34_tile_in_136_array(
                meld_34_copy[1], closed_hand_copy)
            closed_hand_copy.remove(second_tile)

            tiles = [first_tile, second_tile, discarded_tile]

            meld = Meld()
            meld.type = meld_type
            meld.tiles = sorted(tiles)

            melds = self.player.melds + [meld]

            selected_tile = self.player.ai.hand_builder.choose_tile_to_discard(
                new_tiles, closed_hand_copy, melds, print_log=False)

            final_results.append({
                'discard_tile':
                selected_tile,
                'meld_print':
                TilesConverter.to_one_line_string(
                    [meld_34[0] * 4, meld_34[1] * 4, meld_34[2] * 4]),
                'meld':
                meld
            })

        final_results = sorted(final_results,
                               key=lambda x:
                               (x['discard_tile'].shanten, -x['discard_tile'].
                                ukeire, x['discard_tile'].valuation))

        DecisionsLogger.debug(log.MELD_PREPARE,
                              'Options with meld calling',
                              context=final_results)

        return final_results[0]
Exemplo n.º 9
0
    def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand, discarded_tile):
        discarded_tile_34 = discarded_tile // 4

        final_results = []
        for meld_34 in possible_melds:
            meld_34_copy = meld_34.copy()
            closed_hand_copy = closed_hand.copy()

            meld_type = is_chi(meld_34_copy) and Meld.CHI or Meld.PON
            meld_34_copy.remove(discarded_tile_34)

            first_tile = TilesConverter.find_34_tile_in_136_array(meld_34_copy[0], closed_hand_copy)
            closed_hand_copy.remove(first_tile)

            second_tile = TilesConverter.find_34_tile_in_136_array(meld_34_copy[1], closed_hand_copy)
            closed_hand_copy.remove(second_tile)

            tiles = [
                first_tile,
                second_tile,
                discarded_tile
            ]

            meld = Meld()
            meld.type = meld_type
            meld.tiles = sorted(tiles)

            melds = self.player.melds + [meld]

            selected_tile = self.player.ai.hand_builder.choose_tile_to_discard(
                new_tiles,
                closed_hand_copy,
                melds,
                print_log=False
            )

            final_results.append({
                'discard_tile': selected_tile,
                'meld_print': TilesConverter.to_one_line_string([meld_34[0] * 4, meld_34[1] * 4, meld_34[2] * 4]),
                'meld': meld
            })

        final_results = sorted(final_results, key=lambda x: (x['discard_tile'].shanten,
                                                             -x['discard_tile'].ukeire,
                                                             x['discard_tile'].valuation))

        DecisionsLogger.debug(log.MELD_PREPARE, 'Options with meld calling', context=final_results)

        return final_results[0]
Exemplo n.º 10
0
    def __init__(self, table, seat, dealer_seat):
        self.table = table
        self.seat = seat
        self.dealer_seat = dealer_seat
        self.logger = DecisionsLogger()

        self.erase_state()
Exemplo n.º 11
0
    def determine_strategy(self, tiles_136):
        # 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, we add strategies with the highest priority first
        strategies = []

        if self.player.table.has_open_tanyao:
            strategies.append(
                TanyaoStrategy(BaseStrategy.TANYAO, self.player,
                               self.gpparams))

        strategies.append(
            YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player, self.gpparams))
        strategies.append(
            HonitsuStrategy(BaseStrategy.HONITSU, self.player, self.gpparams))
        strategies.append(
            ChinitsuStrategy(BaseStrategy.CHINITSU, self.player,
                             self.gpparams))

        strategies.append(
            ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, self.player,
                               self.gpparams))
        strategies.append(
            FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player,
                                 self.gpparams))

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

        if self.current_strategy:
            if not old_strategy or self.current_strategy.type != old_strategy.type:
                DecisionsLogger.debug(
                    log.STRATEGY_ACTIVATE,
                    context=self.current_strategy,
                )

        if not self.current_strategy and old_strategy:
            DecisionsLogger.debug(log.STRATEGY_DROP, context=old_strategy)

        return self.current_strategy and True or False
Exemplo n.º 12
0
    def draw_tile(self, tile_136):
        DecisionsLogger.debug(
            log.DRAW,
            context=[
                'Step: {}'.format(self.round_step),
                'Hand: {}'.format(self.format_hand_for_print(tile_136)),
                'In defence: {}'.format(self.ai.in_defence),
                'Current strategy: {}'.format(self.ai.current_strategy)
            ])

        self.last_draw = tile_136
        self.tiles.append(tile_136)

        # we need sort it to have a better string presentation
        self.tiles = sorted(self.tiles)

        self.ai.draw_tile(tile_136)
Exemplo n.º 13
0
    def draw_tile(self, tile_136):
        DecisionsLogger.debug(
            log.DRAW,
            context=[
                'Step: {}'.format(self.round_step),
                'Hand: {}'.format(self.format_hand_for_print(tile_136)),
                'In defence: {}'.format(self.ai.in_defence),
                'Current strategy: {}'.format(self.ai.current_strategy)
            ]
        )

        self.last_draw = tile_136
        self.tiles.append(tile_136)

        # we need sort it to have a better string presentation
        self.tiles = sorted(self.tiles)

        self.ai.draw_tile(tile_136)
    def process_discard_option(self, discard_option, closed_hand, force_discard=False, print_log=True):
        if print_log:
            DecisionsLogger.debug(
                log.DISCARD,
                context=discard_option
            )

        self.player.in_tempai = discard_option.shanten == 0
        self.ai.waiting = discard_option.waiting
        self.ai.shanten = discard_option.shanten
        self.ai.ukeire = discard_option.ukeire
        self.ai.ukeire_second = discard_option.ukeire_second

        # when we called meld we don't need "smart" discard
        if force_discard:
            return discard_option.find_tile_in_hand(closed_hand)

        last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None
        if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard:
            return self.player.last_draw
        else:
            return discard_option.find_tile_in_hand(closed_hand)
Exemplo n.º 15
0
    def process_discard_option(self, discard_option, closed_hand, force_discard=False, print_log=True):
        if print_log:
            DecisionsLogger.debug(
                log.DISCARD,
                context=discard_option
            )

        self.player.in_tempai = discard_option.shanten == 0
        self.ai.waiting = discard_option.waiting
        self.ai.shanten = discard_option.shanten
        self.ai.ukeire = discard_option.ukeire
        self.ai.ukeire_second = discard_option.ukeire_second

        # when we called meld we don't need "smart" discard
        if force_discard:
            return discard_option.find_tile_in_hand(closed_hand)

        last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None
        if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard:
            return self.player.last_draw
        else:
            return discard_option.find_tile_in_hand(closed_hand)
Exemplo n.º 16
0
    def determine_strategy(self, tiles_136):
        # 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, we add strategies with the highest priority first
        strategies = []

        if self.player.table.has_open_tanyao:
            strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player))

        strategies.append(YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player))
        strategies.append(HonitsuStrategy(BaseStrategy.HONITSU, self.player))
        strategies.append(ChinitsuStrategy(BaseStrategy.CHINITSU, self.player))

        strategies.append(ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, self.player))
        strategies.append(FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player))

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

        if self.current_strategy:
            if not old_strategy or self.current_strategy.type != old_strategy.type:
                DecisionsLogger.debug(
                    log.STRATEGY_ACTIVATE,
                    context=self.current_strategy,
                )

        if not self.current_strategy and old_strategy:
            DecisionsLogger.debug(log.STRATEGY_DROP, context=old_strategy)

        return self.current_strategy and True or False
Exemplo n.º 17
0
    def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True):
        """
        Try to find best tile to discard, based on different rules
        """

        discard_options, _ = self.find_discard_options(
            tiles,
            closed_hand,
            melds
        )

        # our strategy can affect discard options
        if self.ai.current_strategy:
            discard_options = self.ai.current_strategy.determine_what_to_discard(
                discard_options,
                closed_hand,
                melds
            )

        had_to_be_discarded_tiles = [x for x in discard_options if x.had_to_be_discarded]
        if had_to_be_discarded_tiles:
            discard_options = sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.ukeire, x.valuation))
            DecisionsLogger.debug(
                log.DISCARD_OPTIONS,
                'Discard marked tiles first',
                discard_options,
                print_log=print_log
            )
            return discard_options[0]

        # remove needed tiles from discard options
        discard_options = [x for x in discard_options if not x.had_to_be_saved]

        discard_options = sorted(discard_options, key=lambda x: (x.shanten, -x.ukeire))
        first_option = discard_options[0]
        results_with_same_shanten = [x for x in discard_options if x.shanten == first_option.shanten]

        possible_options = [first_option]
        ukeire_borders = self._choose_ukeire_borders(first_option, 20, 'ukeire')
        for discard_option in results_with_same_shanten:
            # there is no sense to check already chosen tile
            if discard_option.tile_to_discard == first_option.tile_to_discard:
                continue

            # let's choose tiles that are close to the max ukeire tile
            if discard_option.ukeire >= first_option.ukeire - ukeire_borders:
                possible_options.append(discard_option)

        if first_option.shanten in [1, 2, 3]:
            ukeire_field = 'ukeire_second'
            for x in possible_options:
                self.calculate_second_level_ukeire(x, tiles, melds)

            possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field))

            filter_percentage = 20
            possible_options = self._filter_list_by_percentage(
                possible_options,
                ukeire_field,
                filter_percentage
            )
        else:
            ukeire_field = 'ukeire'
            possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field))

        # only one option - so we choose it
        if len(possible_options) == 1:
            return possible_options[0]

        # tempai state has a special handling
        if first_option.shanten == 0:
            other_tiles_with_same_shanten = [x for x in possible_options if x.shanten == 0]
            return self._choose_best_discard_in_tempai(tiles, melds, other_tiles_with_same_shanten)

        tiles_without_dora = [x for x in possible_options if x.count_of_dora == 0]

        # we have only dora candidates to discard
        if not tiles_without_dora:
            DecisionsLogger.debug(
                log.DISCARD_OPTIONS,
                context=possible_options,
                print_log=print_log
            )

            min_dora = min([x.count_of_dora for x in possible_options])
            min_dora_list = [x for x in possible_options if x.count_of_dora == min_dora]

            return sorted(min_dora_list, key=lambda x: -getattr(x, ukeire_field))[0]

        # only one option - so we choose it
        if len(tiles_without_dora) == 1:
            return tiles_without_dora[0]

        # 1-shanten hands have special handling - we can consider future hand cost here
        if first_option.shanten == 1:
            return sorted(tiles_without_dora, key=lambda x: (-x.second_level_cost, -x.ukeire_second, x.valuation))[0]

        if first_option.shanten == 2 or first_option.shanten == 3:
            # we filter 10% of options here
            second_filter_percentage = 10
            filtered_options = self._filter_list_by_percentage(
                tiles_without_dora,
                ukeire_field,
                second_filter_percentage
            )
        # we should also consider borders for 3+ shanten hands
        else:
            best_option_without_dora = tiles_without_dora[0]
            ukeire_borders = self._choose_ukeire_borders(best_option_without_dora, 10, ukeire_field)
            filtered_options = []
            for discard_option in tiles_without_dora:
                val = getattr(best_option_without_dora, ukeire_field) - ukeire_borders
                if getattr(discard_option, ukeire_field) >= val:
                    filtered_options.append(discard_option)

        DecisionsLogger.debug(
            log.DISCARD_OPTIONS,
            context=possible_options,
            print_log=print_log
        )

        closed_hand_34 = TilesConverter.to_34_array(closed_hand)
        isolated_tiles = [x for x in filtered_options if is_tile_strictly_isolated(closed_hand_34, x.tile_to_discard)]
        # isolated tiles should be discarded first
        if isolated_tiles:
            # let's sort tiles by value and let's choose less valuable tile to discard
            return sorted(isolated_tiles, key=lambda x: x.valuation)[0]

        # there are no isolated tiles or we don't care about them
        # let's discard tile with greater ukeire/ukeire2
        filtered_options = sorted(filtered_options, key=lambda x: -getattr(x, ukeire_field))
        first_option = filtered_options[0]

        other_tiles_with_same_ukeire = [x for x in filtered_options
                                        if getattr(x, ukeire_field) == getattr(first_option, ukeire_field)]

        # it will happen with shanten=1, all tiles will have ukeire_second == 0
        # or in tempai we can have several tiles with same ukeire
        if other_tiles_with_same_ukeire:
            return sorted(other_tiles_with_same_ukeire, key=lambda x: x.valuation)[0]

        # we have only one candidate to discard with greater ukeire
        return first_option
    def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True):
        """
        Try to find best tile to discard, based on different rules
        """

        discard_options, _ = self.find_discard_options(
            tiles,
            closed_hand,
            melds
        )

        # our strategy can affect discard options
        if self.ai.current_strategy:
            discard_options = self.ai.current_strategy.determine_what_to_discard(
                discard_options,
                closed_hand,
                melds
            )

        had_to_be_discarded_tiles = [x for x in discard_options if x.had_to_be_discarded]
        if had_to_be_discarded_tiles:
            discard_options = sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.ukeire, x.valuation))
            DecisionsLogger.debug(
                log.DISCARD_OPTIONS,
                'Discard marked tiles first',
                discard_options,
                print_log=print_log
            )
            return discard_options[0]

        # remove needed tiles from discard options
        discard_options = [x for x in discard_options if not x.had_to_be_saved]

        discard_options = sorted(discard_options, key=lambda x: (x.shanten, -x.ukeire))
        first_option = discard_options[0]
        results_with_same_shanten = [x for x in discard_options if x.shanten == first_option.shanten]

        possible_options = [first_option]
        ukeire_borders = self._choose_ukeire_borders(first_option, 20, 'ukeire')
        for discard_option in results_with_same_shanten:
            # there is no sense to check already chosen tile
            if discard_option.tile_to_discard == first_option.tile_to_discard:
                continue

            # let's choose tiles that are close to the max ukeire tile
            if discard_option.ukeire >= first_option.ukeire - ukeire_borders:
                possible_options.append(discard_option)

        if first_option.shanten in [1, 2, 3]:
            ukeire_field = 'ukeire_second'
            for x in possible_options:
                self.calculate_second_level_ukeire(x, tiles, melds)

            possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field))

            filter_percentage = 20
            possible_options = self._filter_list_by_percentage(
                possible_options,
                ukeire_field,
                filter_percentage
            )
        else:
            ukeire_field = 'ukeire'
            possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field))

        # only one option - so we choose it
        if len(possible_options) == 1:
            return possible_options[0]

        # tempai state has a special handling
        if first_option.shanten == 0:
            other_tiles_with_same_shanten = [x for x in possible_options if x.shanten == 0]
            return self._choose_best_discard_in_tempai(tiles, melds, other_tiles_with_same_shanten)

        tiles_without_dora = [x for x in possible_options if x.count_of_dora == 0]

        # we have only dora candidates to discard
        if not tiles_without_dora:
            DecisionsLogger.debug(
                log.DISCARD_OPTIONS,
                context=possible_options,
                print_log=print_log
            )

            min_dora = min([x.count_of_dora for x in possible_options])
            min_dora_list = [x for x in possible_options if x.count_of_dora == min_dora]

            return sorted(min_dora_list, key=lambda x: -getattr(x, ukeire_field))[0]

        # only one option - so we choose it
        if len(tiles_without_dora) == 1:
            return tiles_without_dora[0]

        # 1-shanten hands have special handling - we can consider future hand cost here
        if first_option.shanten == 1:
            return sorted(tiles_without_dora, key=lambda x: (-x.second_level_cost, -x.ukeire_second, x.valuation))[0]

        if first_option.shanten == 2 or first_option.shanten == 3:
            # we filter 10% of options here
            second_filter_percentage = 10
            filtered_options = self._filter_list_by_percentage(
                tiles_without_dora,
                ukeire_field,
                second_filter_percentage
            )
        # we should also consider borders for 3+ shanten hands
        else:
            best_option_without_dora = tiles_without_dora[0]
            ukeire_borders = self._choose_ukeire_borders(best_option_without_dora, 10, ukeire_field)
            filtered_options = []
            for discard_option in tiles_without_dora:
                val = getattr(best_option_without_dora, ukeire_field) - ukeire_borders
                if getattr(discard_option, ukeire_field) >= val:
                    filtered_options.append(discard_option)

        DecisionsLogger.debug(
            log.DISCARD_OPTIONS,
            context=possible_options,
            print_log=print_log
        )

        closed_hand_34 = TilesConverter.to_34_array(closed_hand)
        isolated_tiles = [x for x in filtered_options if is_tile_strictly_isolated(closed_hand_34, x.tile_to_discard)]
        # isolated tiles should be discarded first
        if isolated_tiles:
            # let's sort tiles by value and let's choose less valuable tile to discard
            return sorted(isolated_tiles, key=lambda x: x.valuation)[0]

        # there are no isolated tiles or we don't care about them
        # let's discard tile with greater ukeire/ukeire2
        filtered_options = sorted(filtered_options, key=lambda x: -getattr(x, ukeire_field))
        first_option = filtered_options[0]

        other_tiles_with_same_ukeire = [x for x in filtered_options
                                        if getattr(x, ukeire_field) == getattr(first_option, ukeire_field)]

        # it will happen with shanten=1, all tiles will have ukeire_second == 0
        # or in tempai we can have several tiles with same ukeire
        if other_tiles_with_same_ukeire:
            return sorted(other_tiles_with_same_ukeire, key=lambda x: x.valuation)[0]

        # we have only one candidate to discard with greater ukeire
        return first_option