Beispiel #1
0
 def parse_kan(self, data, meld):
     base_and_called = data >> 8
     base = base_and_called // 4
     meld.type = Meld.KAN
     meld.tiles = [
         Tile(4 * base),
         Tile(1 + 4 * base),
         Tile(2 + 4 * base),
         Tile(3 + 4 * base)
     ]
Beispiel #2
0
 def parse_chi(self, data, meld):
     meld.type = Meld.CHI
     t0, t1, t2 = (data >> 3) & 0x3, (data >> 5) & 0x3, (data >> 7) & 0x3
     base_and_called = data >> 10
     base = base_and_called // 3
     base = (base // 7) * 9 + base % 7
     meld.tiles = [
         Tile(t0 + 4 * (base + 0)),
         Tile(t1 + 4 * (base + 1)),
         Tile(t2 + 4 * (base + 2))
     ]
Beispiel #3
0
    def add_discarded_tile(self,
                           player_seat,
                           tile,
                           is_tsumogiri,
                           drawn_tile=None):
        """
        :param player_seat:
        :param tile: 136 format tile
        :param is_tsumogiri: was tile discarded from hand or not
        :param drawn_tile: tile drawn from table
        """
        logger.debug(
            "  check 1: %s, %s" %
            (self.count_of_remaining_tiles, len(self.remaining_tiles)))
        self.count_of_remaining_tiles -= 1
        if drawn_tile is not None:
            self.remaining_tiles.remove(drawn_tile)  # add this info

        logger.debug("  add_discarded_tile:%s,%s,%s,%s" %
                     (player_seat, tile, is_tsumogiri, drawn_tile))
        logger.debug(
            "  check 2: %s, %s" %
            (self.count_of_remaining_tiles, len(self.remaining_tiles)))

        tile = Tile(tile, is_tsumogiri)
        self.get_player(player_seat).add_discarded_tile(tile)

        # cache already revealed tiles
        self._add_revealed_tile(tile.value)
Beispiel #4
0
def test_dont_meld_agari():
    """
    We shouldn't open when we are already in tempai expect for some special cases
    """
    table = Table()
    table.player.dealer_seat = 3

    strategy = FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, table.player)

    tiles = string_to_136_array(man="2379", sou="4568", pin="22299")
    table.player.init_hand(tiles)

    # Let's move to 15th round step
    for _ in range(0, 15):
        table.player.add_discarded_tile(Tile(0, False))

    assert strategy.should_activate_strategy(table.player.tiles) is True

    tiles = string_to_136_array(man="23789", sou="456", pin="22299")
    table.player.init_hand(tiles)

    meld = make_meld(MeldPrint.CHI, man="789")
    table.player.add_called_meld(meld)

    tile = string_to_136_tile(man="4")
    meld, _ = table.player.try_to_call_meld(tile, True)
    assert meld is None
    def emulate_discard(self, discard_option):
        player_tiles_original = self.player.tiles[:]
        player_discards_original = self.player.discards[:]

        tile_in_hand = discard_option.tile_to_discard_136

        self.player.discards.append(Tile(tile_in_hand, False))
        self.player.tiles.remove(tile_in_hand)

        return player_tiles_original, player_discards_original
Beispiel #6
0
def test_should_activate_strategy():
    table = Table()
    table.player.dealer_seat = 3

    strategy = FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, table.player)

    tiles = string_to_136_array(sou="12355689", man="89", pin="339")
    table.player.init_hand(tiles)
    assert strategy.should_activate_strategy(table.player.tiles) is False

    # Let's move to 10th round step
    for _ in range(0, 10):
        table.player.add_discarded_tile(Tile(0, False))

    assert strategy.should_activate_strategy(table.player.tiles) is False

    # Now we move to 11th turn, we have 2 shanten and no doras,
    # we should go for formal tempai
    table.player.add_discarded_tile(Tile(0, True))
    assert strategy.should_activate_strategy(table.player.tiles) is True
    def test_should_activate_strategy(self):
        strategy = FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI,
                                        self.player)

        tiles = self._string_to_136_array(sou='12355689', man='89', pin='339')
        self.player.init_hand(tiles)
        self.assertEqual(strategy.should_activate_strategy(self.player.tiles),
                         False)

        # Let's move to 10th round step
        for _ in range(0, 10):
            self.player.add_discarded_tile(Tile(0, False))

        self.assertEqual(strategy.should_activate_strategy(self.player.tiles),
                         False)

        # Now we move to 11th turn, we have 2 shanten and no doras,
        # we should go for formal tempai
        self.player.add_discarded_tile(Tile(0, True))
        self.assertEqual(strategy.should_activate_strategy(self.player.tiles),
                         True)
    def add_discarded_tile(self, player_seat, tile, is_tsumogiri):
        """
        :param player_seat:
        :param tile: 136 format tile
        :param is_tsumogiri: was tile discarded from hand or not
        """
        self.count_of_remaining_tiles -= 1

        tile = Tile(tile, is_tsumogiri)
        self.get_player(player_seat).add_discarded_tile(tile)

        # cache already revealed tiles
        self._add_revealed_tile(tile.value)
Beispiel #9
0
def test_call_riichi_chiitoitsu_with_suji():
    table = _make_table(dora_indicators=[
        string_to_136_tile(man="1"),
    ])

    for _ in range(0, 3):
        table.add_discarded_tile(1, string_to_136_tile(honors="3"), False)

    tiles = string_to_136_array(man="22336688", sou="9", pin="99", honors="22")
    table.player.init_hand(tiles)
    table.player.add_discarded_tile(Tile(string_to_136_tile(sou="6"), True))

    table.player.draw_tile(string_to_136_tile(honors="3"))
    table.player.discard_tile()
    assert table.player.can_call_riichi() is True
    def test_call_riichi_chiitoitsu_with_suji(self):
        self._make_table(dora_indicators=[
            self._string_to_136_tile(man='1'),
        ])

        for _ in range(0, 3):
            self.table.add_discarded_tile(1, self._string_to_136_tile(honors='3'), False)

        tiles = self._string_to_136_array(man='22336688', sou='9', pin='99', honors='22')
        self.player.init_hand(tiles)
        self.player.add_discarded_tile(Tile(self._string_to_136_tile(sou='6'), True))

        self.player.draw_tile(self._string_to_136_tile(honors='3'))
        self.player.discard_tile()
        self.assertEqual(self.player.can_call_riichi(), True)
    def test_get_tempai(self):
        tiles = self._string_to_136_array(man='2379', sou='4568', pin='22299')
        self.player.init_hand(tiles)

        # Let's move to 15th round step
        for _ in range(0, 15):
            self.player.add_discarded_tile(Tile(0, False))

        tile = self._string_to_136_tile(man='8')
        meld, _ = self.player.try_to_call_meld(tile, True)
        self.assertNotEqual(meld, None)
        self.assertEqual(self._to_string(meld.tiles), '789m')

        tile_to_discard = self.player.discard_tile()
        self.assertEqual(self._to_string([tile_to_discard]), '8s')
Beispiel #12
0
def _choose_tanki_with_suji_helper(tiles, suji_tiles, tile_to_draw,
                                   tile_to_discard_str):
    table = Table()
    player = table.player
    player.round_step = 2
    player.dealer_seat = 3

    player.init_hand(tiles)

    for tile in suji_tiles:
        player.add_discarded_tile(Tile(tile, True))

    player.draw_tile(tile_to_draw)
    discarded_tile, _ = player.discard_tile()
    assert tiles_to_string([discarded_tile]) == tile_to_discard_str
Beispiel #13
0
def _choose_furiten_over_karaten_helper(tiles, furiten_tile, karaten_tile,
                                        tile_to_draw, tile_to_discard_str):
    table = Table()
    player = table.player
    player.round_step = 2
    player.dealer_seat = 3

    player.init_hand(tiles)

    player.add_discarded_tile(Tile(furiten_tile, True))

    for _ in range(0, 3):
        table.add_discarded_tile(1, karaten_tile, False)

    player.draw_tile(tile_to_draw)
    discarded_tile, _ = player.discard_tile()
    assert tiles_to_string([discarded_tile]) == tile_to_discard_str
    def _avoid_furiten_helper(self, tiles, furiten_tile, other_tile,
                              tile_to_draw, tile_to_discard_str):
        table = Table()
        player = table.player
        player.round_step = 2
        player.dealer_seat = 3

        player.init_hand(tiles)

        player.add_discarded_tile(Tile(furiten_tile, True))

        for _ in range(0, 2):
            table.add_discarded_tile(1, other_tile, False)

        player.draw_tile(tile_to_draw)
        discarded_tile = player.discard_tile()
        self.assertEqual(self._to_string([discarded_tile]),
                         tile_to_discard_str)
Beispiel #15
0
    def add_discarded_tile(self, player_seat, tile_136, is_tsumogiri):
        """
        :param player_seat:
        :param tile_136: 136 format tile
        :param is_tsumogiri: was tile discarded from hand or not
        """
        if player_seat != 0:
            self.count_of_remaining_tiles -= 1

        tile = Tile(tile_136, is_tsumogiri)
        player = self.get_player(player_seat)
        player.add_discarded_tile(tile)

        self._add_revealed_tile(tile_136)

        if self.latest_riichi_player_seat == player_seat:
            self.latest_riichi_player_seat = None
            player.riichi_tile_136 = tile_136

        player.is_ippatsu = False
Beispiel #16
0
 def parse_pon(self, data, meld):
     t4 = (data >> 5) & 0x3
     t0, t1, t2 = ((1, 2, 3), (0, 2, 3), (0, 1, 3), (0, 1, 2))[t4]
     base_and_called = data >> 9
     base = base_and_called // 3
     if data & 0x8:
         meld.type = Meld.PON
         meld.tiles = [
             Tile(t0 + 4 * base),
             Tile(t1 + 4 * base),
             Tile(t2 + 4 * base)
         ]
     else:
         meld.type = Meld.CHAKAN
         meld.tiles = [
             Tile(t0 + 4 * base),
             Tile(t1 + 4 * base),
             Tile(t2 + 4 * base),
             Tile(t4 + 4 * base)
         ]
    def test_dont_meld_agari(self):
        strategy = FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI,
                                        self.player)

        tiles = self._string_to_136_array(man='2379', sou='4568', pin='22299')
        self.player.init_hand(tiles)

        # Let's move to 15th round step
        for _ in range(0, 15):
            self.player.add_discarded_tile(Tile(0, False))

        self.assertEqual(strategy.should_activate_strategy(self.player.tiles),
                         True)

        tiles = self._string_to_136_array(man='23789', sou='456', pin='22299')
        self.player.init_hand(tiles)

        meld = self._make_meld(Meld.CHI, man='789')
        self.player.add_called_meld(meld)

        tile = self._string_to_136_tile(man='4')
        meld, _ = self.player.try_to_call_meld(tile, True)
        self.assertEqual(meld, None)
Beispiel #18
0
def test_get_tempai():
    table = Table()
    table.player.dealer_seat = 3

    tiles = string_to_136_array(man="2379", sou="4568", pin="22299")
    table.player.init_hand(tiles)

    # Let's move to 15th round step
    for _ in range(0, 15):
        table.player.add_discarded_tile(Tile(0, False))

    tile = string_to_136_tile(man="8")
    meld, _ = table.player.try_to_call_meld(tile, True)
    assert meld is not None
    assert tiles_to_string(meld.tiles) == "789m"

    # reinit hand with meld
    tiles = string_to_136_array(man="23789", sou="4568", pin="22299")
    table.player.init_hand(tiles)
    table.player.add_called_meld(meld)

    tile_to_discard, _ = table.player.discard_tile()
    assert tiles_to_string([tile_to_discard]) == "8s"
Beispiel #19
0
    def test_open_hand_on_fifth_round_step(self):
        """
        If we have valuable pair in the hand, 1+ dora and 5+ round step
        let's open on this valuable pair
        """

        tiles = self._string_to_136_array(man='59',
                                          sou='1235',
                                          pin='12789',
                                          honors='11')
        self.player.init_hand(tiles)

        tile = self._string_to_136_tile(honors='1')
        meld, _ = self.player.try_to_call_meld(tile, True)
        self.assertEqual(meld, None)

        # add doras to the hand
        self.table.dora_indicators.append(self._string_to_136_tile(pin='7'))
        self.player.init_hand(tiles)

        tile = self._string_to_136_tile(honors='1')
        meld, _ = self.player.try_to_call_meld(tile, True)
        self.assertEqual(meld, None)

        # one discard == one round step
        self.player.add_discarded_tile(Tile(0, False))
        self.player.add_discarded_tile(Tile(0, False))
        self.player.add_discarded_tile(Tile(0, False))
        self.player.add_discarded_tile(Tile(0, False))
        self.player.add_discarded_tile(Tile(0, False))
        self.player.add_discarded_tile(Tile(0, False))
        self.player.init_hand(tiles)

        # after 5 round step we can open hand
        tile = self._string_to_136_tile(honors='1')
        meld, _ = self.player.try_to_call_meld(tile, True)
        self.assertNotEqual(meld, None)

        # but we don't need to open hand for atodzuke here
        tile = self._string_to_136_tile(pin='3')
        meld, _ = self.player.try_to_call_meld(tile, True)
        self.assertEqual(meld, None)
def test_open_hand_on_fifth_round_step():
    """
    If we have valuable pair in the hand, 1+ dora and 5+ round step
    let's open on this valuable pair
    """
    table = Table()
    table.player.dealer_seat = 3

    tiles = string_to_136_array(man="59", sou="1235", pin="12789", honors="11")
    table.player.init_hand(tiles)

    tile = string_to_136_tile(honors="1")
    meld, _ = table.player.try_to_call_meld(tile, True)
    assert meld is None

    # add doras to the hand
    table.dora_indicators.append(string_to_136_tile(pin="7"))
    table.player.init_hand(tiles)

    tile = string_to_136_tile(honors="1")
    meld, _ = table.player.try_to_call_meld(tile, True)
    assert meld is None

    # one discard == one round step
    table.player.add_discarded_tile(Tile(0, False))
    table.player.add_discarded_tile(Tile(0, False))
    table.player.add_discarded_tile(Tile(0, False))
    table.player.add_discarded_tile(Tile(0, False))
    table.player.add_discarded_tile(Tile(0, False))
    table.player.add_discarded_tile(Tile(0, False))
    table.player.init_hand(tiles)

    # after 5 round step we can open hand
    tile = string_to_136_tile(honors="1")
    meld, _ = table.player.try_to_call_meld(tile, True)
    assert meld is not None

    # but we don't need to open hand for atodzuke here
    tile = string_to_136_tile(pin="3")
    meld, _ = table.player.try_to_call_meld(tile, True)
    assert meld is None
Beispiel #21
0
 def draw_tile(self, tile):
     self.tiles.append(Tile(tile))
     # we need sort it to have a better string presentation
     self.tiles = sorted(self.tiles)
Beispiel #22
0
 def add_discarded_tile(self, tile):
     self.discards.append(Tile(tile))
Beispiel #23
0
 def init_hand(self, tiles):
     self.tiles = [Tile(i) for i in tiles]
Beispiel #24
0
 def parse_nuki(self, data, meld):
     meld.type = Meld.NUKI
     meld.tiles = [Tile(data >> 8)]
Beispiel #25
0
    def _choose_best_discard_in_tempai(self, discard_options, after_meld):
        # first of all we find tiles that have the best hand cost * ukeire value
        call_riichi = not (self.player.is_open_hand or after_meld)

        discard_desc = []
        player_tiles_copy = self.player.tiles[:]

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

        for discard_option in discard_options:
            tile = discard_option.find_tile_in_hand(self.player.closed_hand)
            # temporary remove discard option to estimate hand value
            self.player.tiles = player_tiles_copy[:]
            self.player.tiles.remove(tile)
            # for kabe/suji handling
            discarded_tile = Tile(tile, False)
            self.player.discards.append(discarded_tile)

            is_furiten = self._is_discard_option_furiten(discard_option)

            if len(discard_option.waiting) == 1:
                waiting = discard_option.waiting[0]

                cost_x_ukeire, hand_cost = self._estimate_cost_x_ukeire(discard_option, call_riichi)

                # let's check if this is a tanki wait
                results, tiles_34 = self.divide_hand(self.player.tiles, waiting)
                result = results[0]

                tanki_type = None

                is_tanki = False
                for hand_set in result:
                    if waiting not in hand_set:
                        continue

                    if is_pair(hand_set):
                        is_tanki = True

                        if is_honor(waiting):
                            # TODO: differentiate between self honor and honor for all players
                            if waiting in self.player.valued_honors:
                                tanki_type = TankiWait.TANKI_WAIT_ALL_YAKUHAI
                            else:
                                tanki_type = TankiWait.TANKI_WAIT_NON_YAKUHAI
                            break

                        simplified_waiting = simplify(waiting)
                        have_suji, have_kabe = self.check_suji_and_kabe(closed_tiles_34, waiting)

                        # TODO: not sure about suji/kabe priority, so we keep them same for now
                        if 3 <= simplified_waiting <= 5:
                            if have_suji or have_kabe:
                                tanki_type = TankiWait.TANKI_WAIT_456_KABE
                            else:
                                tanki_type = TankiWait.TANKI_WAIT_456_RAW
                        elif 2 <= simplified_waiting <= 6:
                            if have_suji or have_kabe:
                                tanki_type = TankiWait.TANKI_WAIT_37_KABE
                            else:
                                tanki_type = TankiWait.TANKI_WAIT_37_RAW
                        elif 1 <= simplified_waiting <= 7:
                            if have_suji or have_kabe:
                                tanki_type = TankiWait.TANKI_WAIT_28_KABE
                            else:
                                tanki_type = TankiWait.TANKI_WAIT_28_RAW
                        else:
                            if have_suji or have_kabe:
                                tanki_type = TankiWait.TANKI_WAIT_69_KABE
                            else:
                                tanki_type = TankiWait.TANKI_WAIT_69_RAW
                        break

                tempai_descriptor = {
                    "discard_option": discard_option,
                    "hand_cost": hand_cost,
                    "cost_x_ukeire": cost_x_ukeire,
                    "is_furiten": is_furiten,
                    "is_tanki": is_tanki,
                    "tanki_type": tanki_type,
                    "max_danger": discard_option.danger.get_max_danger(),
                    "sum_danger": discard_option.danger.get_sum_danger(),
                    "weighted_danger": discard_option.danger.get_weighted_danger(),
                }
                discard_desc.append(tempai_descriptor)
            else:
                cost_x_ukeire, _ = self._estimate_cost_x_ukeire(discard_option, call_riichi)

                tempai_descriptor = {
                    "discard_option": discard_option,
                    "hand_cost": None,
                    "cost_x_ukeire": cost_x_ukeire,
                    "is_furiten": is_furiten,
                    "is_tanki": False,
                    "tanki_type": None,
                    "max_danger": discard_option.danger.get_max_danger(),
                    "sum_danger": discard_option.danger.get_sum_danger(),
                    "weighted_danger": discard_option.danger.get_weighted_danger(),
                }
                discard_desc.append(tempai_descriptor)

            # save descriptor to discard option for future users
            discard_option.tempai_descriptor = tempai_descriptor

            # reverse all temporary tile tweaks
            self.player.tiles = player_tiles_copy
            self.player.discards.remove(discarded_tile)

        discard_desc = sorted(discard_desc, key=lambda k: (-k["cost_x_ukeire"], k["is_furiten"], k["weighted_danger"]))

        # if we don't have any good options, e.g. all our possible waits are karaten
        if discard_desc[0]["cost_x_ukeire"] == 0:
            # we still choose between options that give us tempai, because we may be going to formal tempai
            # with no hand cost
            return self._choose_safest_tile(discard_options)

        num_tanki_waits = len([x for x in discard_desc if x["is_tanki"]])

        # what if all our waits are tanki waits? we need a special handling for that case
        if num_tanki_waits == len(discard_options):
            return self._choose_best_tanki_wait(discard_desc)

        best_discard_desc = [x for x in discard_desc if x["cost_x_ukeire"] == discard_desc[0]["cost_x_ukeire"]]
        best_discard_desc = sorted(best_discard_desc, key=lambda k: (k["is_furiten"], k["weighted_danger"]))

        # if we have several options that give us similar wait
        return best_discard_desc[0]["discard_option"]
    def calculate_second_level_ukeire(self, discard_option, after_meld=False):
        self._assert_hand_correctness()

        not_suitable_tiles = self.ai.current_strategy and self.ai.current_strategy.not_suitable_tiles or []
        call_riichi = discard_option.with_riichi

        # we are going to do manipulations that require player hand and discards to be updated
        # so we save original tiles and discards here and restore it at the end of the function
        player_tiles_original, player_discards_original = self.emulate_discard(
            discard_option)

        sum_tiles = 0
        sum_cost = 0
        average_costs = []
        for wait_34 in discard_option.waiting:
            if self.player.is_open_hand and wait_34 in not_suitable_tiles:
                continue

            closed_hand_34 = TilesConverter.to_34_array(
                self.player.closed_hand)
            live_tiles = 4 - self.player.number_of_revealed_tiles(
                wait_34, closed_hand_34)

            if live_tiles == 0:
                continue

            wait_136 = self._find_live_tile(wait_34)
            assert wait_136 is not None
            self.player.tiles.append(wait_136)

            results, shanten = self.find_discard_options()
            results = [
                x for x in results if x.shanten == discard_option.shanten - 1
            ]

            # let's take best ukeire here
            if results:
                result_has_atodzuke = False
                if self.player.is_open_hand:
                    best_one = results[0]
                    best_ukeire = 0
                    for result in results:
                        has_atodzuke = False
                        ukeire = 0
                        for wait_34 in result.waiting:
                            if wait_34 in not_suitable_tiles:
                                has_atodzuke = True
                            else:
                                ukeire += result.wait_to_ukeire[wait_34]

                        # let's consider atodzuke waits to be worse than non-atodzuke ones
                        if has_atodzuke:
                            ukeire /= 2

                        # FIXME consider sorting by cost_x_ukeire as well
                        if (ukeire > best_ukeire) or (ukeire >= best_ukeire
                                                      and not has_atodzuke):
                            best_ukeire = ukeire
                            best_one = result
                            result_has_atodzuke = has_atodzuke
                else:
                    if shanten == 0:
                        # FIXME save cost_x_ukeire to not calculate it twice
                        best_one = sorted(
                            results,
                            key=lambda x:
                            (-x.ukeire, -self._estimate_cost_x_ukeire(
                                x, call_riichi=call_riichi)[0]),
                        )[0]
                    else:
                        best_one = sorted(results, key=lambda x: -x.ukeire)[0]
                    best_ukeire = best_one.ukeire

                sum_tiles += best_ukeire * live_tiles

                # if we are going to have a tempai (on our second level) - let's also count its cost
                if shanten == 0:
                    # temporary update players hand and discards for calculations
                    next_tile_in_hand = best_one.tile_to_discard_136
                    tile_for_discard = Tile(next_tile_in_hand, False)
                    self.player.tiles.remove(next_tile_in_hand)
                    self.player.discards.append(tile_for_discard)

                    cost_x_ukeire, _ = self._estimate_cost_x_ukeire(
                        best_one, call_riichi=call_riichi)
                    if best_ukeire != 0:
                        average_costs.append(cost_x_ukeire / best_ukeire)
                    # we reduce tile valuation for atodzuke
                    if result_has_atodzuke:
                        cost_x_ukeire /= 2
                    sum_cost += cost_x_ukeire

                    # restore original players hand and discard state
                    self.player.tiles.append(next_tile_in_hand)
                    self.player.discards.remove(tile_for_discard)

            self.player.tiles.remove(wait_136)

        discard_option.ukeire_second = sum_tiles
        if discard_option.shanten == 1:
            if discard_option.ukeire != 0:
                discard_option.average_second_level_waits = round(
                    sum_tiles / discard_option.ukeire, 2)

            discard_option.second_level_cost = sum_cost
            if not average_costs:
                discard_option.average_second_level_cost = 0
            else:
                discard_option.average_second_level_cost = int(
                    sum(average_costs) / len(average_costs))

        # restore original state of player hand and discards
        self.restore_after_emulate_discard(player_tiles_original,
                                           player_discards_original)
    def _choose_best_discard_in_tempai(self, tiles, melds, discard_options):
        # first of all we find tiles that have the best hand cost * ukeire value
        call_riichi = not self.player.is_open_hand

        discard_desc = []
        player_tiles_copy = self.player.tiles.copy()
        player_melds_copy = self.player.melds.copy()

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

        for discard_option in discard_options:
            tile = discard_option.find_tile_in_hand(self.player.closed_hand)
            # temporary remove discard option to estimate hand value
            self.player.tiles = tiles.copy()
            self.player.tiles.remove(tile)
            # temporary replace melds
            self.player.melds = melds.copy()
            # for kabe/suji handling
            discarded_tile = Tile(tile, False)
            self.player.discards.append(discarded_tile)

            is_furiten = self._is_discard_option_furiten(discard_option)

            if len(discard_option.waiting) == 1:
                waiting = discard_option.waiting[0]

                cost_x_ukeire, hand_cost = self._estimate_cost_x_ukeire(discard_option, call_riichi)

                # let's check if this is a tanki wait
                results, tiles_34 = self.divide_hand(self.player.tiles, waiting)
                result = results[0]

                tanki_type = None

                is_tanki = False
                for hand_set in result:
                    if waiting not in hand_set:
                        continue

                    if is_pair(hand_set):
                        is_tanki = True

                        if is_honor(waiting):
                            # TODO: differentiate between self honor and honor for all players
                            if waiting in self.player.valued_honors:
                                tanki_type = self.TankiWait.TANKI_WAIT_ALL_YAKUHAI
                            else:
                                tanki_type = self.TankiWait.TANKI_WAIT_NON_YAKUHAI
                            break

                        simplified_waiting = simplify(waiting)
                        have_suji, have_kabe = self.check_suji_and_kabe(closed_tiles_34, waiting)

                        # TODO: not sure about suji/kabe priority, so we keep them same for now
                        if 3 <= simplified_waiting <= 5:
                            if have_suji or have_kabe:
                                tanki_type = self.TankiWait.TANKI_WAIT_456_KABE
                            else:
                                tanki_type = self.TankiWait.TANKI_WAIT_456_RAW
                        elif 2 <= simplified_waiting <= 6:
                            if have_suji or have_kabe:
                                tanki_type = self.TankiWait.TANKI_WAIT_37_KABE
                            else:
                                tanki_type = self.TankiWait.TANKI_WAIT_37_RAW
                        elif 1 <= simplified_waiting <= 7:
                            if have_suji or have_kabe:
                                tanki_type = self.TankiWait.TANKI_WAIT_28_KABE
                            else:
                                tanki_type = self.TankiWait.TANKI_WAIT_28_RAW
                        else:
                            if have_suji or have_kabe:
                                tanki_type = self.TankiWait.TANKI_WAIT_69_KABE
                            else:
                                tanki_type = self.TankiWait.TANKI_WAIT_69_RAW
                        break

                discard_desc.append({
                    'discard_option': discard_option,
                    'hand_cost': hand_cost,
                    'cost_x_ukeire': cost_x_ukeire,
                    'is_furiten': is_furiten,
                    'is_tanki': is_tanki,
                    'tanki_type': tanki_type
                })
            else:
                cost_x_ukeire, _ = self._estimate_cost_x_ukeire(discard_option, call_riichi)

                discard_desc.append({
                    'discard_option': discard_option,
                    'hand_cost': None,
                    'cost_x_ukeire': cost_x_ukeire,
                    'is_furiten': is_furiten,
                    'is_tanki': False,
                    'tanki_type': None
                })

            # reverse all temporary tile tweaks
            self.player.tiles = player_tiles_copy
            self.player.melds = player_melds_copy
            self.player.discards.remove(discarded_tile)

        discard_desc = sorted(discard_desc, key=lambda k: (k['cost_x_ukeire'], not k['is_furiten']), reverse=True)

        # if we don't have any good options, e.g. all our possible waits ara karaten
        # FIXME: in that case, discard the safest tile
        if discard_desc[0]['cost_x_ukeire'] == 0:
            return sorted(discard_options, key=lambda x: x.valuation)[0]

        num_tanki_waits = len([x for x in discard_desc if x['is_tanki']])

        # what if all our waits are tanki waits? we need a special handling for that case
        if num_tanki_waits == len(discard_options):
            return self._choose_best_tanki_wait(discard_desc)

        best_discard_desc = [x for x in discard_desc if x['cost_x_ukeire'] == discard_desc[0]['cost_x_ukeire']]

        # we only have one best option based on ukeire and cost, nothing more to do here
        if len(best_discard_desc) == 1:
            return best_discard_desc[0]['discard_option']

        # if we have several options that give us similar wait
        # FIXME: 1. we find the safest tile to discard
        # FIXME: 2. if safeness is the same, we try to discard non-dora tiles
        return best_discard_desc[0]['discard_option']