Example #1
0
async def read_item(winning_tile: str,
                    man_tile: str = None,
                    pin_tile: str = None,
                    sou_tile: str = None):
    calculator = HandCalculator()
    # アガリ形(man=マンズ, pin=ピンズ, sou=ソーズ, honors=字牌)
    tiles = TilesConverter.string_to_136_array(man=man_tile,
                                               pin=pin_tile,
                                               sou=sou_tile)

    # アガリ牌(ソーズの5)
    win_tile = TilesConverter.string_to_136_array(sou=winning_tile)[0]

    # 鳴き(チー:CHI, ポン:PON, カン:KAN(True:ミンカン,False:アンカン), カカン:CHANKAN, ヌキドラ:NUKI)
    melds = None

    # ドラ(なし)
    dora_indicators = None

    # オプション(なし)
    config = HandConfig(is_tsumo=True)

    result = calculator.estimate_hand_value(tiles, win_tile, melds,
                                            dora_indicators, config)

    return {
        "result_main": result.cost['main'],
        "result_additional": result.cost['additional'],
        "yaku": result.yaku
    }
    def find_tile_in_hand(self, closed_hand):
        """
        Find and return 136 tile in closed player hand
        """

        if self.player.table.has_aka_dora:
            tiles_five_of_suits = [4, 13, 22]
            # special case, to keep aka dora in hand
            if self.tile_to_discard in tiles_five_of_suits:
                aka_closed_hand = closed_hand[:]
                while True:
                    tile = TilesConverter.find_34_tile_in_136_array(self.tile_to_discard, aka_closed_hand)

                    # we have only aka dora in the hand, without simple five
                    if not tile:
                        break

                    # we found aka in the hand,
                    # let's try to search another five tile
                    # to keep aka dora
                    if tile in AKA_DORA_LIST:
                        aka_closed_hand.remove(tile)
                    else:
                        return tile

        return TilesConverter.find_34_tile_in_136_array(self.tile_to_discard, closed_hand)
Example #3
0
    def find_tile_in_hand(self, closed_hand):
        """
        Find and return 136 tile in closed player hand
        """

        if settings.FIVE_REDS:
            # special case, to keep aka dora in hand
            if self.tile_to_discard in [4, 13, 22]:
                aka_closed_hand = closed_hand[:]
                while True:
                    tile = TilesConverter.find_34_tile_in_136_array(
                        self.tile_to_discard, aka_closed_hand)
                    # we have only aka dora in the hand
                    if not tile:
                        break

                    # we found aka in the hand,
                    # let's try to search another five tile
                    # to keep aka dora
                    if tile in AKA_DORA_LIST:
                        aka_closed_hand.remove(tile)
                    else:
                        return tile

        return TilesConverter.find_34_tile_in_136_array(
            self.tile_to_discard, closed_hand)
Example #4
0
    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
Example #5
0
def hand_calculator(tiles, win_tile, config=HandConfig()):
    calculator = HandCalculator()
    tiles = TilesConverter.one_line_string_to_136_array(str(tiles),
                                                        has_aka_dora=True)
    win_tile = TilesConverter.one_line_string_to_136_array(
        str(win_tile), has_aka_dora=True)[0]
    return calculator.estimate_hand_value(tiles, win_tile, config=config)
def check_pinfu(man, pin, sou, honors, player_wind, round_wind, win_tile_type,
                win_tile_value):
    calculator = HandCalculator()

    tiles = TilesConverter.string_to_136_array(man=man,
                                               pin=pin,
                                               sou=sou,
                                               honors=honors)
    print(tiles)
    win_tile = TilesConverter.string_to_136_array(
        **{win_tile_type: win_tile_value})[0]

    config = HandConfig(player_wind=player_wind, round_wind=round_wind)
    result = calculator.estimate_hand_value(tiles, win_tile, config=config)

    if result.yaku is not None:
        for yaku in result.yaku:
            if yaku.name == "Pinfu":
                cost = 1500 if config.is_dealer else 1000
                return [
                    json.dumps({
                        'isPinfu': True,
                        'cost': cost
                    }).encode("utf-8")
                ]

    return [json.dumps({'isPinfu': False, 'cost': 0}).encode("utf-8")]
    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
Example #8
0
    def find_tile_in_hand(self, closed_hand):
        """
        Find and return 136 tile in closed player hand
        """

        if self.player.table.has_aka_dora:
            tiles_five_of_suits = [4, 13, 22]
            # special case, to keep aka dora in hand
            if self.tile_to_discard in tiles_five_of_suits:
                aka_closed_hand = closed_hand[:]
                while True:
                    tile = TilesConverter.find_34_tile_in_136_array(
                        self.tile_to_discard, aka_closed_hand)

                    # we have only aka dora in the hand, without simple five
                    if not tile:
                        break

                    # we found aka in the hand,
                    # let's try to search another five tile
                    # to keep aka dora
                    if tile in AKA_DORA_LIST:
                        aka_closed_hand.remove(tile)
                    else:
                        return tile

        return TilesConverter.find_34_tile_in_136_array(
            self.tile_to_discard, closed_hand)
Example #9
0
    def discard_tile(self, discard_tile=None):
        """
        :param discard_tile: 136 tile format
        :return:
        """

        tile_to_discard = self.ai.discard_tile(discard_tile)

        is_tsumogiri = tile_to_discard == self.last_draw
        # it is important to use table method,
        # to recalculate revealed tiles and etc.
        self.table.add_discarded_tile(0, tile_to_discard, is_tsumogiri)

        # debug defence with honors
        if tile_to_discard in self.tiles:
            self.tiles.remove(tile_to_discard)
        else:
            logger.info("Catch that bug: defence with honors")
            logger.info("Hand: {}\nTile: {}".format(
                TilesConverter.to_one_line_string(self.tiles),
                TilesConverter.to_one_line_string([tile_to_discard])))
            logger.info("Hand: {}\nTile: {}".format(self.tiles,
                                                    tile_to_discard))
            return self.tiles[-1]

        return tile_to_discard
Example #10
0
    def test_one_line_string_to_136_array(self):
        initial_string = '789m456p555s11222z'
        tiles = TilesConverter.one_line_string_to_136_array(initial_string)
        self.assertEqual(len(tiles), 14)

        new_string = TilesConverter.to_one_line_string(tiles)
        self.assertEqual(initial_string, new_string)
Example #11
0
    def discard_tile(self):
        h = Hand(TilesConverter.to_one_line_string(self.player.tiles))

        tiles = TilesConverter.to_34_array(self.player.tiles)
        shanten = self.shanten.calculate_shanten(tiles)
        if shanten == 0:
            self.player.in_tempai = True

        if h.test_win():
            return Shanten.AGARI_STATE
        elif self.player.in_tempai:
            results, st = self.calculate_outs()
            tile34 = results[0]['discard']
            tile_in_hand = TilesConverter.find_34_tile_in_136_array(
                tile34, self.player.tiles)
            return tile_in_hand
        else:
            hand_data = h.get_data()
            it = int(
                self.model.predict_classes(transformCSVHandToCNNMatrix(
                    expandHandToCSV(hand_data)),
                                           verbose=0)[0])
            t = hand_data[it]
            tile_in_hand = self.mahjong_tile_to_discard_tile(t)
            return tile_in_hand
Example #12
0
def translateIntoMsg(res: dict) -> str:
    Msg = {}
    finalStr = ''
    if -1 in res.keys():
        arr = res[-1]
        res.pop(-1)
        finalStr += ' 摸 '
        for i in arr:
            finalStr += '%s' % TilesConverter.to_one_line_string([i])
    if -2 in res.keys():
        finalStr += '\n %s' % res[-2]
        return finalStr
    for k in res:
        name = TilesConverter.to_one_line_string([k])
        tiles = TilesConverter.to_34_array(res[k])
        for i in range(0, len(tiles)):
            tiles[i] = 1 if tiles[i] > 0 else 0
        tiles = TilesConverter.to_136_array(tiles)
        chance = ''
        for i in tiles:
            chance += TilesConverter.to_one_line_string([i])
        if len(res[k]) in Msg.keys():
            Msg[len(res[k])].append('打%s 摸[' % name + chance +
                                    ' %d枚]' % len(res[k]))
        else:
            Msg[len(
                res[k])] = ['打%s 摸[' % name + chance + ' %d枚]' % len(res[k])]
    keys = sorted(Msg, reverse=True)

    for k in keys:
        for item in Msg[k]:
            finalStr += '\n' + item
    return finalStr
Example #13
0
    def calculate_outs(self):
        tiles = TilesConverter.to_34_array(self.player.tiles)

        shanten = self.shanten.calculate_shanten(tiles)
        # win
        if shanten == Shanten.AGARI_STATE:
            return [], shanten

        raw_data = {}
        for i in range(0, 34):
            if not tiles[i]:
                continue

            tiles[i] -= 1

            raw_data[i] = []
            for j in range(0, 34):
                if i == j or tiles[j] >= 4:
                    continue

                tiles[j] += 1
                if self.shanten.calculate_shanten(tiles) == shanten - 1:
                    raw_data[i].append(j)
                tiles[j] -= 1

            tiles[i] += 1

            if raw_data[i]:
                raw_data[i] = {
                    'tile': i,
                    'tiles_count': self.count_tiles(raw_data[i], tiles),
                    'waiting': raw_data[i]
                }

        results = []
        tiles = TilesConverter.to_34_array(self.player.tiles)
        for tile in range(0, len(tiles)):
            if tile in raw_data and raw_data[tile] and raw_data[tile][
                    'tiles_count']:
                item = raw_data[tile]

                waiting = []

                for item2 in item['waiting']:
                    waiting.append(item2)

                results.append({
                    'discard': item['tile'],
                    'waiting': waiting,
                    'tiles_count': item['tiles_count']
                })

        # if we have character and honor candidates to discard with same tiles count,
        # we need to discard honor tile first
        results = sorted(results,
                         key=lambda x: (x['tiles_count'], x['discard']),
                         reverse=True)

        return results, shanten
Example #14
0
    def test_find_34_tile_in_136_array(self):
        result = TilesConverter.find_34_tile_in_136_array(0, [3, 4, 5, 6])
        self.assertEqual(result, 3)

        result = TilesConverter.find_34_tile_in_136_array(33, [3, 4, 134, 135])
        self.assertEqual(result, 134)

        result = TilesConverter.find_34_tile_in_136_array(20, [3, 4, 134, 135])
        self.assertEqual(result, None)
    def find_discard_options(self):
        """
        :param tiles: array of tiles in 136 format
        :param closed_hand: array of tiles in 136 format
        :return:
        """
        self._assert_hand_correctness()

        tiles = self.player.tiles
        closed_hand = self.player.closed_hand

        tiles_34 = TilesConverter.to_34_array(tiles)
        closed_tiles_34 = TilesConverter.to_34_array(closed_hand)
        is_agari = self.ai.agari.is_agari(tiles_34, self.player.meld_34_tiles)

        # we decide beforehand if we need to consider chiitoitsu for all of our possible discards
        min_shanten, use_chiitoitsu = self.calculate_shanten_and_decide_hand_structure(
            closed_tiles_34)

        results = []
        tile_34_prev = None
        # we iterate in reverse order to naturally handle aka-doras, i.e. discard regular 5 if we have it
        for tile_136 in reversed(self.player.closed_hand):
            tile_34 = tile_136 // 4
            # already added
            if tile_34 == tile_34_prev:
                continue
            else:
                tile_34_prev = tile_34

            closed_tiles_34[tile_34] -= 1
            waiting, shanten = self.calculate_waits(
                closed_tiles_34, tiles_34, use_chiitoitsu=use_chiitoitsu)
            assert shanten >= min_shanten
            closed_tiles_34[tile_34] += 1

            if waiting:
                wait_to_ukeire = dict(
                    zip(waiting, [
                        self.count_tiles([x], closed_tiles_34) for x in waiting
                    ]))
                results.append(
                    DiscardOption(
                        player=self.player,
                        shanten=shanten,
                        tile_to_discard_136=tile_136,
                        waiting=waiting,
                        ukeire=sum(wait_to_ukeire.values()),
                        wait_to_ukeire=wait_to_ukeire,
                    ))

        if is_agari:
            shanten = Shanten.AGARI_STATE
        else:
            shanten = min_shanten

        return results, shanten
Example #16
0
def to_34_array(obj=None, man=None, pin=None, sou=None, honors=None):
    if obj == None:
        return TilesConverter.string_to_34_array(man=man,
                                                 pin=pin,
                                                 sou=sou,
                                                 honors=honors)
    else:
        return TilesConverter.string_to_34_array(man=obj["man"],
                                                 pin=obj["pin"],
                                                 sou=obj["sou"])
Example #17
0
 def __init__(self, tiles, dora_indicators, revealed_tiles):
     self.tiles = tiles
     self.Shanten_calculater = Shanten()
     self.Hand_Calculator = HandCalculator()
     self.Tiles = TilesConverter()
     self.shanten = self.Shanten_calculater.calculate_shanten(
         self.Tiles.to_34_array(self.tiles))
     self.Hand_Config = HandConfig(is_riichi=True)
     self.dora_indicators = dora_indicators
     self.revealed_tiles = revealed_tiles
Example #18
0
    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
            tiles_34[tile] -= 1
            h = sum(
                self.simulate(tiles_34, self.player.open_hand_34_tiles,
                              unaccounted) for _ in range(200))
            tiles_34[tile] += 1
            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
Example #19
0
def getTile(s: str) -> dict:
    #translate s into supported format
    if len(s) == 0:
        return {}
    tiles = TilesConverter.one_line_string_to_136_array(s, True)
    test = {}
    # check tiles length
    added = []
    if len(tiles) % 3 == 0:
        #3n, add a random tile
        while True:
            tmp = randint(0, 135)
            if tmp not in tiles:
                tiles.append(tmp)
                added.append(tmp)
                break
    if len(tiles) % 3 == 1:
        #3n+1, add a random tile
        while True:
            tmp = randint(0, 135)
            if tmp not in tiles:
                tiles.append(tmp)
                added.append(tmp)
                break
    if len(added) > 0:
        test[-1] = added
    # now is a normal form
    tiles.sort()
    avaliable = []
    for i in range(0, 136):
        if i not in tiles:
            avaliable.append(i)
    calculator = Shanten()
    baseShanten = calculator.calculate_shanten(
        TilesConverter.to_34_array(tiles))
    if baseShanten == -1:
        return {-2: '已和牌'}
    #14*122 try

    tmp = copy.deepcopy(tiles)
    for t in tiles:
        tmp.remove(t)
        one_try = []
        for i in avaliable:
            tmp.append(i)
            res = calculator.calculate_shanten(
                TilesConverter.to_34_array(sorted(tmp)))
            if res < baseShanten:
                one_try.append(i)
            tmp.remove(i)
        t = 4 * (t // 4)
        if len(one_try) > 0 and t not in test.keys():
            test[t] = copy.deepcopy(one_try)
        tmp.append(t)
    return test
Example #20
0
    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
            tiles_34[tile] -= 1
            h = sum(
                self.simulate(tiles_34, open_hand_34, unaccounted)
                for _ in range(200))
            tiles_34[tile] += 1
            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
Example #21
0
 def format_hand_for_print(self, tile):
     hand_string = '{} + {}'.format(
         TilesConverter.to_one_line_string(self.closed_hand),
         TilesConverter.to_one_line_string([tile]))
     if self.is_open_hand:
         melds = []
         for item in self.melds:
             melds.append('{}'.format(
                 TilesConverter.to_one_line_string(item.tiles)))
         hand_string += ' [{}]'.format(', '.join(melds))
     return hand_string
Example #22
0
 def set_state(self, play_state):
     logger.info("Set the player's state from {} to {}".format(
         self.play_state, play_state))
     logger.info("Hand: {}".format(
         TilesConverter.to_one_line_string(self.closed_hand)))
     logger.info("Outs: {} {}".format(
         TilesConverter.to_one_line_string(
             [out * 4 for out in self.ai.waiting]),
         self.ai.wanted_tiles_count))
     self.play_state = play_state
     self.latest_change_index = len(self.discards)
Example #23
0
 def _format_hand_for_print(self, tiles, new_tile, melds):
     tiles_string = TilesConverter.to_one_line_string(tiles, print_aka_dora=self.player.table.has_aka_dora)
     tile_string = TilesConverter.to_one_line_string([new_tile], print_aka_dora=self.player.table.has_aka_dora)
     hand_string = f"{tiles_string} + {tile_string}"
     hand_string += " [{}]".format(
         ", ".join(
             [
                 TilesConverter.to_one_line_string(x.tiles, print_aka_dora=self.player.table.has_aka_dora)
                 for x in melds
             ]
         )
     )
     return hand_string
Example #24
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]
Example #25
0
    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
Example #26
0
    def format_hand_for_print(self, tile_136=None):
        hand_string = '{}'.format(TilesConverter.to_one_line_string(self.closed_hand))

        if tile_136 is not None:
            hand_string += ' + {}'.format(TilesConverter.to_one_line_string([tile_136]))

        melds = []
        for item in self.melds:
            melds.append('{}'.format(TilesConverter.to_one_line_string(item.tiles)))

        if melds:
            hand_string += ' [{}]'.format(', '.join(melds))

        return hand_string
Example #27
0
def main():
    hand = random.choice(questions)
    round_wind, player_wind = get_random_kaze_set()
    tiles = TilesConverter.string_to_136_array(
        man=hand.get_man(), pin=hand.get_pin(), sou=hand.get_sou())
    win_tile = TilesConverter.string_to_136_array(
        **hand.get_win_tile())[0]
    conf = {
        'is_tsumo': get_is_or_not(),
        'is_riichi': get_is_or_not(),
        'player_wind': player_wind,
        'round_wind': round_wind
    }
    config = HandConfig(**conf)
    result = calculator.estimate_hand_value(tiles, win_tile, config=config)

    question_caption = '\n'
    if config.is_tsumo:
        question_caption += f"ツモ:{hand.get_win_tile_figure()} "
    else:
        question_caption += f"ロン:{hand.get_win_tile_figure()} "

    if config.is_riichi:
        question_caption += 'リーチ有 '
    else:
        question_caption += 'リーチ無 '
    question_caption += f"場風: {DISPLAY_WINDS_JP[config.round_wind]} 自風: {DISPLAY_WINDS_JP[config.player_wind]}"

    print(hand.get_figure())
    print(question_caption)
    if config.is_tsumo and config.player_wind == EAST:
        child_answer = int(input('子の支払う点数: '))
        if child_answer == result.cost['main']:
            print('正解!!')
        else:
            print(f"不正解!! 正解は {result.cost['main']} オール")
    elif config.is_tsumo and config.player_wind != EAST:
        parent_answer = int(input('親の支払う点数: '))
        child_answer = int(input('子の支払う点数: '))
        if parent_answer == result.cost['main'] and child_answer == result.cost['additional']:
            print('正解!!')
        else:
            print(
                f"不正解!! 正解は 親: {result.cost['main']}, 子: {result.cost['additional']}")
    else:
        answer = int(input('放銃者の支払う点数: '))
        if answer == result.cost['main']:
            print('正解!!')
        else:
            print(f"不正解!! 正解は {result.cost['main']}")
Example #28
0
    def calculate_outs(self, tiles, closed_hand, open_sets_34=None):
        """
        :param tiles: array of tiles in 136 format
        :param closed_hand: array of tiles in 136 format
        :param open_sets_34: array of array with tiles in 34 format
        :return:
        """
        tiles_34 = TilesConverter.to_34_array(tiles)
        closed_tiles_34 = TilesConverter.to_34_array(closed_hand)
        is_agari = self.agari.is_agari(tiles_34,
                                       self.player.open_hand_34_tiles)

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

            tiles_34[hand_tile] -= 1

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

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

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

            tiles_34[hand_tile] += 1

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

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

        return results, shanten
    def _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]
Example #30
0
 def draw_tile(self, tile):
     """
     :param tile: 136 tile format
     :return:
     """
     self.player.in_tempai = False
     self.shanten_calculator = Shanten()
     Tile = TilesConverter()
     current_shanten = self.shanten_calculator.calculate_shanten(
         Tile.to_34_array(self.player.tiles))
     logger.debug('Current_shanten:{}'.format(current_shanten))
     if current_shanten == 0:
         self.player.in_tempai = True
     logger.debug('whether_in_tempai:{}'.format(self.player.in_tempai))
     self.shanten_calculator = None
Example #31
0
    def is_threatening(self):
        """
        Should we fold against this player or not
        :return: boolean
        """
        if self.player.in_riichi:
            return True

        discards = self.player.discards
        discards_34 = TilesConverter.to_34_array([x.value for x in discards])

        is_honitsu_open_sets, open_hand_suit = False, None
        is_honitsu_discards, discard_suit = self._is_honitsu_discards(
            discards_34)

        meld_tiles = self.player.meld_tiles
        meld_tiles_34 = TilesConverter.to_34_array(meld_tiles)
        if meld_tiles:
            dora_count = sum(
                [plus_dora(x, self.table.dora_indicators) for x in meld_tiles])
            # aka dora
            dora_count += sum([
                1 for x in meld_tiles
                if is_aka_dora(x, self.table.has_open_tanyao)
            ])
            # enemy has a lot of dora tiles in his opened sets
            # so better to fold against him
            if dora_count >= 3:
                return True

            # check that user has a discard and melds that looks like honitsu
            is_honitsu_open_sets, open_hand_suit = self._is_honitsu_open_sets(
                meld_tiles_34)

        if is_honitsu_open_sets:
            # for 2 opened melds we had to check discard, to be sure
            if len(
                    self.player.melds
            ) <= 2 and is_honitsu_discards and discard_suit == open_hand_suit:
                self.chosen_suit = open_hand_suit
                return True

            # for 3+ opened melds there is no sense to check discard
            if len(self.player.melds) >= 3:
                self.chosen_suit = open_hand_suit
                return True

        return False
Example #32
0
 def serialize(self):
     data = {
         "tile":
         TilesConverter.to_one_line_string(
             [self.tile_to_discard_136],
             print_aka_dora=self.player.table.has_aka_dora),
         "shanten":
         self.shanten,
         "ukeire":
         self.ukeire,
         "valuation":
         self.valuation,
         "danger": {
             "max_danger": self.danger.get_max_danger(),
             "sum_danger": self.danger.get_sum_danger(),
             "weighted_danger": self.danger.get_weighted_danger(),
             "min_border": self.danger.get_min_danger_border(),
             "danger_border": self.danger.danger_border,
             "weighted_cost": self.danger.weighted_cost,
             "danger_reasons": self.danger.values,
             "can_be_used_for_ryanmen": self.danger.can_be_used_for_ryanmen,
         },
     }
     if self.ukeire_second:
         data["ukeire2"] = self.ukeire_second
     if self.average_second_level_waits:
         data[
             "average_second_level_waits"] = self.average_second_level_waits
     if self.average_second_level_cost:
         data["average_second_level_cost"] = self.average_second_level_cost
     if self.had_to_be_saved:
         data["had_to_be_saved"] = self.had_to_be_saved
     if self.had_to_be_discarded:
         data["had_to_be_discarded"] = self.had_to_be_discarded
     return data
    def should_call_riichi(self):
        # empty waiting can be found in some cases
        if not self.player.ai.waiting:
            return False

        if self.player.ai.in_defence:
            return False

        # don't call karaten riichi
        count_tiles = self.player.ai.hand_builder.count_tiles(
            self.player.ai.waiting,
            TilesConverter.to_34_array(self.player.closed_hand)
        )
        if count_tiles == 0:
            return False

        # It is daburi!
        first_discard = self.player.round_step == 1
        if first_discard and not self.player.table.meld_was_called:
            return True

        if len(self.player.ai.waiting) == 1:
            return self._should_call_riichi_one_sided()

        return self._should_call_riichi_many_sided()
    def _initialize_honitsu_dora_count(self, tiles_136, suit):
        tiles_34 = TilesConverter.to_34_array(tiles_136)

        dora_count_man_not_isolated = 0
        dora_count_pin_not_isolated = 0
        dora_count_sou_not_isolated = 0

        for tile_136 in tiles_136:
            tile_34 = tile_136 // 4

            dora_count = plus_dora(tile_136, self.player.table.dora_indicators)

            if is_aka_dora(tile_136, self.player.table.has_aka_dora):
                dora_count += 1

            if is_man(tile_34):
                if not is_tile_strictly_isolated(tiles_34, tile_34):
                    dora_count_man_not_isolated += dora_count

            if is_pin(tile_34):
                if not is_tile_strictly_isolated(tiles_34, tile_34):
                    dora_count_pin_not_isolated += dora_count

            if is_sou(tile_34):
                if not is_tile_strictly_isolated(tiles_34, tile_34):
                    dora_count_sou_not_isolated += dora_count

        if suit['name'] == 'pin':
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated
        elif suit['name'] == 'sou':
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated
        elif suit['name'] == 'man':
            self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated
    def meld_had_to_be_called(self, tile):
        tile //= 4
        tiles_34 = TilesConverter.to_34_array(self.player.tiles)
        valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2]

        # for big shanten number we don't need to check already opened pon set,
        # because it will improve our hand anyway
        if self.player.ai.shanten < 2:
            for meld in self.player.melds:
                # we have already opened yakuhai pon
                # so we don't need to open hand without shanten improvement
                if self._is_yakuhai_pon(meld):
                    return False

        # if we don't have any yakuhai pon and this is our last chance, we must call this tile
        if tile in self.last_chance_calls:
            return True

        # in all other cases for closed hand we don't need to open hand with special conditions
        if not self.player.is_open_hand:
            return False

        # we have opened the hand already and don't yet have yakuhai pon
        # so we now must get it
        for valued_pair in valued_pairs:
            if valued_pair == tile:
                return True

        return False
    def should_activate_strategy(self, tiles_136):
        """
        We can go for chiitoitsu strategy if we have 5 pairs
        """

        result = super(ChiitoitsuStrategy, self).should_activate_strategy(tiles_136)
        if not result:
            return False

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

        num_pairs = len([x for x in range(0, 34) if tiles_34[x] == 2])
        num_pons = len([x for x in range(0, 34) if tiles_34[x] == 3])

        # for now we don't consider chiitoitsu with less than 5 pair
        if num_pairs < 5:
            return False

        # if we have 5 pairs and tempai, this is obviously not chiitoitsu
        if num_pairs == 5 and self.player.ai.shanten == 0:
            return False

        # for now we won't go for chiitoitsu if we have 5 pairs and pon
        if num_pairs == 5 and num_pons > 0:
            return False

        return True
    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
    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))
 def __unicode__(self):
     tile_format_136 = TilesConverter.to_one_line_string([self.tile_to_discard*4])
     return 'tile={}, shanten={}, ukeire={}, ukeire2={}, valuation={}'.format(
         tile_format_136,
         self.shanten,
         self.ukeire,
         self.ukeire_second,
         self.valuation
     )
    def find_discard_options(self, tiles, closed_hand, melds=None):
        """
        :param tiles: array of tiles in 136 format
        :param closed_hand: array of tiles in 136 format
        :param melds:
        :return:
        """
        if melds is None:
            melds = []

        open_sets_34 = [x.tiles_34 for x in melds]

        tiles_34 = TilesConverter.to_34_array(tiles)
        closed_tiles_34 = TilesConverter.to_34_array(closed_hand)
        is_agari = self.ai.agari.is_agari(tiles_34, self.player.meld_34_tiles)

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

            tiles_34[hand_tile] -= 1
            waiting, shanten = self.calculate_waits(tiles_34, open_sets_34)
            tiles_34[hand_tile] += 1

            if waiting:
                wait_to_ukeire = dict(zip(waiting, [self.count_tiles([x], closed_tiles_34) for x in waiting]))
                results.append(DiscardOption(player=self.player,
                                             shanten=shanten,
                                             tile_to_discard=hand_tile,
                                             waiting=waiting,
                                             ukeire=self.count_tiles(waiting, closed_tiles_34),
                                             wait_to_ukeire=wait_to_ukeire))

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

        return results, shanten
    def divide_hand(self, tiles, waiting):
        tiles_copy = tiles.copy()

        for i in range(0, 4):
            if waiting * 4 + i not in tiles_copy:
                tiles_copy += [waiting * 4 + i]
                break

        tiles_34 = TilesConverter.to_34_array(tiles_copy)

        results = self.player.ai.hand_divider.divide_hand(tiles_34)
        return results, tiles_34
    def _should_call_riichi_many_sided(self):
        count_tiles = self.player.ai.hand_builder.count_tiles(
            self.player.ai.waiting,
            TilesConverter.to_34_array(self.player.closed_hand)
        )
        hand_costs = []
        waits_with_yaku = 0
        for waiting in self.player.ai.waiting:
            hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False)
            if hand_value.error is None:
                hand_costs.append(hand_value.cost['main'])
                if hand_value.yaku is not None and hand_value.cost is not None:
                    waits_with_yaku += 1

        # if we have yaku on every wait
        if waits_with_yaku == len(self.player.ai.waiting):
            min_cost = min(hand_costs)

            # let's not riichi this bad wait
            if count_tiles <= 2:
                return False

            # if wait is slighly better, we will riichi only a cheap hand
            if count_tiles <= 4:
                if self.player.is_dealer and min_cost >= 7700:
                    return False

                if not self.player.is_dealer and min_cost >= 5200:
                    return False

                return True

            # wait is even better, but still don't call riichi on damaten mangan
            if count_tiles <= 6:
                if self.player.is_dealer and min_cost >= 11600:
                    return False

                if not self.player.is_dealer and min_cost >= 7700:
                    return False

                return True

            # if wait is good we only damaten haneman
            if self.player.is_dealer and min_cost >= 18000:
                return False

            if not self.player.is_dealer and min_cost >= 12000:
                return False

            return True

        # if we don't have yaku on every wait and it's two-sided or more, we call riichi
        return True
    def is_threatening(self):
        """
        Should we fold against this player or not
        :return: boolean
        """
        if self.player.in_riichi:
            return True

        discards = self.player.discards
        discards_34 = TilesConverter.to_34_array([x.value for x in discards])

        is_honitsu_open_sets, open_hand_suit = False, None
        is_honitsu_discards, discard_suit = self._is_honitsu_discards(discards_34)

        meld_tiles = self.player.meld_tiles
        meld_tiles_34 = TilesConverter.to_34_array(meld_tiles)
        if meld_tiles:
            dora_count = sum([plus_dora(x, self.table.dora_indicators) for x in meld_tiles])
            # aka dora
            dora_count += sum([1 for x in meld_tiles if is_aka_dora(x, self.table.has_open_tanyao)])
            # enemy has a lot of dora tiles in his opened sets
            # so better to fold against him
            if dora_count >= 3:
                return True

            # check that user has a discard and melds that looks like honitsu
            is_honitsu_open_sets, open_hand_suit = self._is_honitsu_open_sets(meld_tiles_34)

        if is_honitsu_open_sets:
            # for 2 opened melds we had to check discard, to be sure
            if len(self.player.melds) <= 2 and is_honitsu_discards and discard_suit == open_hand_suit:
                self.chosen_suit = open_hand_suit
                return True

            # for 3+ opened melds there is no sense to check discard
            if len(self.player.melds) >= 3:
                self.chosen_suit = open_hand_suit
                return True

        return False
    def __str__(self):
        dora_string = TilesConverter.to_one_line_string(self.dora_indicators)

        round_settings = {
            EAST:  ['e', 0],
            SOUTH: ['s', 3],
            WEST:  ['w', 7]
        }.get(self.round_wind_tile)

        round_string, round_diff = round_settings
        display_round = '{}{}'.format(round_string, (self.round_wind_number + 1) - round_diff)

        return 'Round: {}, Honba: {}, Dora Indicators: {}'.format(
            display_round,
            self.count_of_honba_sticks,
            dora_string
        )
    def determine_what_to_discard(self, discard_options, hand, open_melds):
        is_open_hand = len(open_melds) > 0

        tiles_34 = TilesConverter.to_34_array(hand)

        valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2]

        # closed pon sets
        valued_pons = [x for x in self.player.valued_honors if tiles_34[x] == 3]
        # open pon sets
        valued_pons += [x for x in open_melds if x.type == Meld.PON and x.tiles[0] // 4 in self.player.valued_honors]

        acceptable_options = []
        for item in discard_options:
            if is_open_hand:
                if len(valued_pons) == 0:
                    # don't destroy our only yakuhai pair
                    if len(valued_pairs) == 1 and item.tile_to_discard in valued_pairs:
                        continue
                elif len(valued_pons) == 1:
                    # don't destroy our only yakuhai pon
                    if item.tile_to_discard in valued_pons:
                        continue

                acceptable_options.append(item)

        # we don't have a choice
        if not acceptable_options:
            return discard_options

        preferred_options = []
        for item in acceptable_options:
            # ignore wait without yakuhai yaku if possible
            if is_open_hand and len(valued_pons) == 0 and len(valued_pairs) == 1:
                if item.shanten == 0:
                    if valued_pairs[0] not in item.waiting:
                        continue

            preferred_options.append(item)

        if not preferred_options:
            return acceptable_options

        return preferred_options
    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_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']
    def should_activate_strategy(self, tiles_136):
        """
        We can go for yakuhai strategy if we have at least one yakuhai pair in the hand
        :return: boolean
        """
        result = super(YakuhaiStrategy, self).should_activate_strategy(tiles_136)
        if not result:
            return False

        tiles_34 = TilesConverter.to_34_array(tiles_136)
        player_hand_tiles_34 = TilesConverter.to_34_array(self.player.tiles)
        player_closed_hand_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand)
        self.valued_pairs = [x for x in self.player.valued_honors if player_hand_tiles_34[x] == 2]

        is_double_east_wind = len([x for x in self.valued_pairs if x == EAST]) == 2
        is_double_south_wind = len([x for x in self.valued_pairs if x == SOUTH]) == 2

        self.valued_pairs = list(set(self.valued_pairs))
        self.has_valued_pon = len([x for x in self.player.valued_honors if player_hand_tiles_34[x] >= 3]) >= 1

        opportunity_to_meld_yakuhai = False

        for x in range(0, 34):
            if x in self.valued_pairs and tiles_34[x] - player_hand_tiles_34[x] == 1:
                opportunity_to_meld_yakuhai = True

        has_valued_pair = False

        for pair in self.valued_pairs:
            # we have valued pair in the hand and there are enough tiles
            # in the wall
            if opportunity_to_meld_yakuhai or self.player.total_tiles(pair, player_closed_hand_tiles_34) < 4:
                has_valued_pair = True
                break

        # we don't have valuable pair or pon to open our hand
        if not has_valued_pair and not self.has_valued_pon:
            return False

        # let's always open double east
        if is_double_east_wind:
            return True

        # let's open double south if we have a dora in the hand
        # or we have other valuable pairs
        if is_double_south_wind and (self.dora_count_total >= 1 or len(self.valued_pairs) >= 2):
            return True

        # If we have 1+ dora in the hand and there are 2+ valuable pairs let's open hand
        if len(self.valued_pairs) >= 2 and self.dora_count_total >= 1:
            return True

        # If we have 2+ dora in the hand let's open hand
        if self.dora_count_total >= 2:
            for x in range(0, 34):
                # we have other pair in the hand
                # so we can open hand for atodzuke
                if player_hand_tiles_34[x] >= 2 and x not in self.valued_pairs:
                    self.go_for_atodzuke = True
            return True

        # If we have 1+ dora in the hand and there is 5+ round step let's open hand
        if self.dora_count_total >= 1 and self.player.round_step > 5:
            return True

        for pair in self.valued_pairs:
            # last chance to get that yakuhai, let's go for it
            if (opportunity_to_meld_yakuhai and
                    self.player.total_tiles(pair, player_closed_hand_tiles_34) == 3 and
                    self.player.ai.shanten >= 1):

                if pair not in self.last_chance_calls:
                    self.last_chance_calls.append(pair)

                return True

        return False
    def should_activate_strategy(self, tiles_136):
        """
        We can go for honitsu strategy if we have prevalence of one suit and honor tiles
        """

        result = super(HonitsuStrategy, self).should_activate_strategy(tiles_136)
        if not result:
            return False

        tiles_34 = TilesConverter.to_34_array(tiles_136)
        suits = count_tiles_by_suits(tiles_34)

        suits = [x for x in suits if x['name'] != 'honor']
        suits = sorted(suits, key=lambda x: x['count'], reverse=True)

        suit = suits[0]

        count_of_shuntsu_other_suits = 0
        count_of_koutsu_other_suits = 0

        count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[1]['function'])
        count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[2]['function'])

        count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[1]['function'])
        count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[2]['function'])

        self._calculate_not_suitable_tiles_cnt(tiles_34, suit['function'])
        self._initialize_honitsu_dora_count(tiles_136, suit)

        # let's not go for honitsu if we have 5 or more non-isolated
        # tiles in other suits
        if self.tiles_count_other_suits >= 5:
            return False

        # let's not go for honitsu if we have 2 or more non-isolated doras
        # in other suits
        if self.dora_count_other_suits_not_isolated >= 2:
            return False

        # if we have a pon of valued doras, let's not go for honitsu
        # we have a mangan anyway, let's go for fastest hand
        valued_pons = [x for x in self.player.valued_honors if tiles_34[x] >= 3]
        for pon in valued_pons:
            dora_count = plus_dora(pon * 4, self.player.table.dora_indicators)
            if dora_count > 0:
                return False

        valued_pairs = len([x for x in self.player.valued_honors if tiles_34[x] == 2])
        honor_pairs_or_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2])
        honor_doras_pairs_or_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2
                                         and plus_dora(x * 4, self.player.table.dora_indicators)])
        unvalued_singles = len([x for x in range(0, 34) if is_honor(x)
                                and x not in self.player.valued_honors
                                and tiles_34[x] == 1])

        # if we have some decent amount of not isolated tiles in other suits
        # we may not rush for honitsu considering other conditions
        if self.tiles_count_other_suits_not_isolated >= 3:
            # if we don't have pair or pon of honored doras
            if honor_doras_pairs_or_pons == 0:
                # we need to either have a valued pair or have at least two honor
                # pairs to consider honitsu
                if valued_pairs == 0 and honor_pairs_or_pons < 2:
                    return False

                # doesn't matter valued or not, if we have just one honor pair
                # and have some single unvalued tiles, let's throw them away
                # first
                if honor_pairs_or_pons == 1 and unvalued_singles >= 2:
                    return False

                # 3 non-isolated unsuitable tiles, 1-shanen and already 8th turn
                # let's not consider honitsu here
                if self.player.ai.shanten == 1 and self.player.round_step > 8:
                    return False
            else:
                # we have a pon of unvalued honor doras, but it looks like
                # it's faster to build our hand without honitsu
                if self.player.ai.shanten == 1:
                    return False

        # if we have a complete set in other suits, we can only throw it away if it's early in the game
        if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1:
            # too late to throw away chi after 8 step
            if self.player.round_step > 8:
                return False

            # already 1 shanten, no need to throw away complete set
            if self.player.ai.shanten == 1:
                return False

            # dora is not isolated and we have a complete set, let's not go for honitsu
            if self.dora_count_other_suits_not_isolated >= 1:
                return False

        self.chosen_suit = suit['function']

        return True
    def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles):
        """
        Determine should we call a meld or not.
        If yes, it will return Meld object and tile to discard
        :param tile: 136 format tile
        :param is_kamicha_discard: boolean
        :param new_tiles:
        :return: Meld and DiscardOption objects
        """
        if self.player.in_riichi:
            return None, None

        if self.player.ai.in_defence:
            return None, None

        closed_hand = self.player.closed_hand[:]

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

        # we can't use this tile for our chosen strategy
        if not self.is_tile_suitable(tile):
            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.player.ai.hand_divider.find_valid_combinations(
                closed_hand_34,
                first_limit,
                second_limit,
                True
            )

        if combinations:
            combinations = combinations[0]

        possible_melds = []
        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 possible_melds:
                    possible_melds.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 possible_melds:
                    possible_melds.append(best_meld_34)

        # we can call melds only with allowed tiles
        validated_melds = []
        for meld in possible_melds:
            if (self.is_tile_suitable(meld[0] * 4) and
                    self.is_tile_suitable(meld[1] * 4) and
                    self.is_tile_suitable(meld[2] * 4)):
                validated_melds.append(meld)
        possible_melds = validated_melds

        if not possible_melds:
            return None, None

        chosen_meld = self._find_best_meld_to_open(possible_melds, new_tiles, closed_hand, tile)
        selected_tile = chosen_meld['discard_tile']
        meld = chosen_meld['meld']

        shanten = selected_tile.shanten
        had_to_be_called = self.meld_had_to_be_called(tile)
        had_to_be_called = had_to_be_called or selected_tile.had_to_be_discarded

        # each strategy can use their own value to min shanten number
        if shanten > self.min_shanten:
            return None, None

        # sometimes we had to call tile, even if it will not improve our hand
        # otherwise we can call only with improvements of shanten
        if not had_to_be_called and shanten >= self.player.ai.shanten:
            return None, None

        return meld, selected_tile
    def should_activate_strategy(self, tiles_136):
        """
        We can go for chinitsu strategy if we have prevalence of one suit
        """

        result = super(ChinitsuStrategy, self).should_activate_strategy(tiles_136)
        if not result:
            return False

        # when making decisions about chinitsu, we should consider
        # the state of our own hand,
        tiles_34 = TilesConverter.to_34_array(self.player.tiles)
        suits = count_tiles_by_suits(tiles_34)

        suits = [x for x in suits if x['name'] != 'honor']
        suits = sorted(suits, key=lambda x: x['count'], reverse=True)
        suit = suits[0]

        count_of_shuntsu_other_suits = 0
        count_of_koutsu_other_suits = 0

        count_of_shuntsu_other_suits += HonitsuStrategy._count_of_shuntsu(tiles_34, suits[1]['function'])
        count_of_shuntsu_other_suits += HonitsuStrategy._count_of_shuntsu(tiles_34, suits[2]['function'])

        count_of_koutsu_other_suits += HonitsuStrategy._count_of_koutsu(tiles_34, suits[1]['function'])
        count_of_koutsu_other_suits += HonitsuStrategy._count_of_koutsu(tiles_34, suits[2]['function'])

        # we need to have at least 9 tiles of one suit to fo for chinitsu
        if suit['count'] < 9:
            return False

        # here we only check doras in different suits, we will deal
        # with honors later
        self._initialize_chinitsu_dora_count(tiles_136, suit)

        # 3 non-isolated doras in other suits is too much
        # to even try
        if self.dora_count_not_suitable >= 3:
            return False

        if self.dora_count_not_suitable == 2:
            # 2 doras in other suits, no doras in our suit
            # let's not consider chinitsu
            if self.dora_count_suitable == 0:
                return False

            # we have 2 doras in other suits and we
            # are 1 shanten, let's not rush chinitsu
            if self.player.ai.shanten == 1:
                return False

            # too late to get rid of doras in other suits
            if self.player.round_step > 8:
                return False

        # we are almost tempai, chinitsu is slower
        if suit['count'] == 9 and self.player.ai.shanten == 1:
            return False

        # only 10 tiles by 8th turn is too slow, considering alternative
        if suit['count'] == 10 and self.player.ai.shanten == 1 and self.player.round_step > 8:
            return False

        # if we have a pon of honors, let's not go for chinitsu
        honor_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 3])
        if honor_pons >= 1:
            return False

        # if we have a valued pair, let's not go for chinitsu
        valued_pairs = len([x for x in self.player.valued_honors if tiles_34[x] == 2])
        if valued_pairs >= 1:
            return False

        # if we have a pair of honor doras, let's not go for chinitsu
        honor_doras_pairs = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] == 2
                                 and plus_dora(x * 4, self.player.table.dora_indicators)])
        if honor_doras_pairs >= 1:
            return False

        # if we have a honor pair, we will only throw them away if it's early in the game
        # and if we have lots of tiles in our suit
        honor_pairs = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] == 2])
        if honor_pairs >= 2:
            return False
        if honor_pairs == 1:
            if suit['count'] < 11:
                return False
            if self.player.round_step > 8:
                return False

        # if we have a complete set in other suits, we can only throw it away if it's early in the game
        if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1:
            # too late to throw away chi after 8 step
            if self.player.round_step > 8:
                return False

            # already 1 shanten, no need to throw away complete set
            if self.player.round_step > 5 and self.player.ai.shanten == 1:
                return False

            # dora is not isolated and we have a complete set, let's not go for chinitsu
            if self.dora_count_not_suitable >= 1:
                return False

        self.chosen_suit = suit['function']

        return True
    def try_to_find_safe_tile_to_discard(self):
        discard_results, _ = self.player.ai.hand_builder.find_discard_options(
            self.player.tiles,
            self.player.closed_hand,
            self.player.melds
        )

        self.hand_34 = TilesConverter.to_34_array(self.player.tiles)
        self.closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)

        threatening_players = self._get_threatening_players()

        # safe tiles that can be safe based on the table situation
        safe_tiles = self.impossible_wait.find_tiles_to_discard(threatening_players)

        # first try to check common safe tiles to discard for all players
        if len(threatening_players) > 1:
            against_honitsu = []
            for player in threatening_players:
                if player.chosen_suit:
                    against_honitsu += [self._mark_safe_tiles_against_honitsu(player)]

            common_safe_tiles = [x.all_safe_tiles for x in threatening_players]
            common_safe_tiles += against_honitsu
            # let's find a common tiles that will be safe against all threatening players
            common_safe_tiles = list(set.intersection(*map(set, common_safe_tiles)))
            common_safe_tiles = [DefenceTile(x, DefenceTile.SAFE) for x in common_safe_tiles]

            # there is no sense to calculate suji tiles for honitsu players
            not_honitsu_players = [x for x in threatening_players if x.chosen_suit is None]
            common_suji_tiles = self.suji.find_tiles_to_discard(not_honitsu_players)

            if common_safe_tiles:
                # it can be that safe tile will be mark as "almost safe",
                # but we already have "safe" tile in our hand
                validated_safe_tiles = common_safe_tiles
                for tile in safe_tiles:
                    already_added_tile = [x for x in common_safe_tiles if x.value == tile.value]
                    if not already_added_tile:
                        validated_safe_tiles.append(tile)

                # first try to check 100% safe tiles for all players
                result = self._find_tile_to_discard(validated_safe_tiles, discard_results)
                if result:
                    return result

            if common_suji_tiles:
                # if there is no 100% safe tiles try to check common suji tiles
                result = self._find_tile_to_discard(common_suji_tiles, discard_results)
                if result:
                    return result

        # there are only one threatening player or we wasn't able to find common safe tiles
        # let's find safe tiles for most dangerous player first
        # and than for all other players if we failed find tile for dangerous player
        for player in threatening_players:
            player_safe_tiles = [DefenceTile(x, DefenceTile.SAFE) for x in player.player.all_safe_tiles]
            player_suji_tiles = self.suji.find_tiles_to_discard([player])

            # it can be that safe tile will be mark as "almost safe",
            # but we already have "safe" tile in our hand
            validated_safe_tiles = player_safe_tiles
            for tile in safe_tiles:
                already_added_tile = [x for x in player_safe_tiles if x.value == tile.value]
                if not already_added_tile:
                    validated_safe_tiles.append(tile)

            # better to not use suji for honitsu hands
            if not player.chosen_suit:
                validated_safe_tiles += player_suji_tiles

            result = self._find_tile_to_discard(validated_safe_tiles, discard_results)
            if result:
                return result

            # try to find safe tiles against honitsu
            if player.chosen_suit:
                against_honitsu = self._mark_safe_tiles_against_honitsu(player)
                against_honitsu = [DefenceTile(x, DefenceTile.SAFE) for x in against_honitsu]

                result = self._find_tile_to_discard(against_honitsu, discard_results)
                if result:
                    return result

        # we wasn't able to find safe tile to discard
        return None
    def calculate_second_level_ukeire(self, discard_option, tiles, melds):
        not_suitable_tiles = self.ai.current_strategy and self.ai.current_strategy.not_suitable_tiles or []
        call_riichi = not self.player.is_open_hand

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

        tile_in_hand = discard_option.find_tile_in_hand(self.player.closed_hand)

        self.player.tiles = tiles.copy()
        self.player.tiles.remove(tile_in_hand)

        sum_tiles = 0
        sum_cost = 0
        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.total_tiles(wait_34, closed_hand_34)

            if live_tiles == 0:
                continue

            wait_136 = wait_34 * 4
            self.player.tiles.append(wait_136)

            results, shanten = self.find_discard_options(
                self.player.tiles,
                self.player.closed_hand,
                melds
            )
            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

                        if (ukeire > best_ukeire) or (ukeire >= best_ukeire and not has_atodzuke):
                            best_ukeire = ukeire
                            best_one = result
                            result_has_atodzuke = has_atodzuke
                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:
                    next_tile_in_hand = best_one.find_tile_in_hand(self.player.closed_hand)
                    self.player.tiles.remove(next_tile_in_hand)
                    cost_x_ukeire, _ = self._estimate_cost_x_ukeire(best_one, call_riichi=call_riichi)
                    # we reduce tile valuation for atodzuke
                    if result_has_atodzuke:
                        cost_x_ukeire /= 2
                    sum_cost += cost_x_ukeire
                    self.player.tiles.append(next_tile_in_hand)

            self.player.tiles.remove(wait_136)

        discard_option.ukeire_second = sum_tiles
        if discard_option.shanten == 1:
            discard_option.second_level_cost = sum_cost

        # restore original state of player hand
        self.player.tiles = player_tiles_original
    def start_game(self):
        log_link = ''

        # play in private or tournament lobby
        if settings.LOBBY != '0':
            if settings.IS_TOURNAMENT:
                logger.info('Go to the tournament lobby: {}'.format(settings.LOBBY))
                self._send_message('<CS lobby="{}" />'.format(settings.LOBBY))
                self._random_sleep(1, 2)
                self._send_message('<DATE />')
            else:
                logger.info('Go to the lobby: {}'.format(settings.LOBBY))
                self._send_message('<CHAT text="{}" />'.format(quote('/lobby {}'.format(settings.LOBBY))))
                self._random_sleep(1, 2)

        if self.reconnected_messages:
            # we already in the game
            self.looking_for_game = False
            self._send_message('<GOK />')
            self._random_sleep(1, 2)
        else:
            selected_game_type = self._build_game_type()
            game_type = '{},{}'.format(settings.LOBBY, selected_game_type)

            if not settings.IS_TOURNAMENT:
                self._send_message('<JOIN t="{}" />'.format(game_type))
                logger.info('Looking for the game...')

            start_time = datetime.datetime.now()

            while self.looking_for_game:
                self._random_sleep(1, 2)

                messages = self._get_multiple_messages()
                for message in messages:
                    if '<REJOIN' in message:
                        # game wasn't found, continue to wait
                        self._send_message('<JOIN t="{},r" />'.format(game_type))

                    if '<GO' in message:
                        self._random_sleep(1, 2)
                        self._send_message('<GOK />')
                        self._send_message('<NEXTREADY />')

                        # we had to have it there
                        # because for tournaments we don't know
                        # what exactly game type was set
                        selected_game_type = self.decoder.parse_go_tag(message)
                        process_rules = self._set_game_rules(selected_game_type)
                        if not process_rules:
                            logger.error('Hirosima (3 man) is not supported at the moment')
                            self.end_game(success=False)
                            return

                    if '<TAIKYOKU' in message:
                        self.looking_for_game = False
                        game_id, seat = self.decoder.parse_log_link(message)
                        log_link = 'http://tenhou.net/0/?log={}&tw={}'.format(game_id, seat)

                        self.statistics.game_id = game_id

                    if '<UN' in message:
                        values = self.decoder.parse_names_and_ranks(message)
                        self.table.set_players_names_and_ranks(values)

                        self.statistics.username = values[0]['name']

                    if '<LN' in message:
                        self._send_message(self._pxr_tag())

                current_time = datetime.datetime.now()
                time_difference = current_time - start_time

                if time_difference.seconds > 60 * settings.WAITING_GAME_TIMEOUT_MINUTES:
                    break

        # we wasn't able to find the game in specified time range
        # sometimes it happens and we need to end process
        # and try again later
        if self.looking_for_game:
            logger.error('Game is not started. Can\'t find the game')
            self.end_game()
            return

        logger.info('Game started')
        logger.info('Log: {}'.format(log_link))
        logger.info('Players: {}'.format(self.table.players))

        main_player = self.table.player

        meld_tile = None
        tile_to_discard = None

        while self.game_is_continue:
            self._random_sleep(1, 2)

            messages = self._get_multiple_messages()

            if self.reconnected_messages:
                messages = self.reconnected_messages + messages
                self.reconnected_messages = None

            if not messages:
                self._count_of_empty_messages += 1
            else:
                # we had set to zero counter
                self._count_of_empty_messages = 0

            for message in messages:
                if '<INIT' in message or '<REINIT' in message:
                    values = self.decoder.parse_initial_values(message)

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

                    logger.info('Round Log: {}&ts={}'.format(log_link, self.table.round_number))

                    tiles = self.decoder.parse_initial_hand(message)
                    self.table.player.init_hand(tiles)

                    logger.info(self.table)
                    logger.info('Players: {}'.format(self.table.get_players_sorted_by_scores()))
                    logger.info('Dealer: {}'.format(self.table.get_player(values['dealer'])))

                if '<REINIT' in message:
                    players = self.decoder.parse_table_state_after_reconnection(message)
                    for x in range(0, 4):
                        player = players[x]
                        for item in player['discards']:
                            self.table.add_discarded_tile(x, item, False)

                        for item in player['melds']:
                            if x == 0:
                                tiles = item.tiles
                                main_player.tiles.extend(tiles)
                            self.table.add_called_meld(x, item)

                # draw and discard
                if '<T' in message:
                    win_suggestions = ['t="16"', 't="48"']
                    # we won by self draw (tsumo)
                    if any(i in message for i in win_suggestions):
                        self._random_sleep(1, 2)
                        self._send_message('<N type="7" />')
                        continue

                    # Kyuushuu kyuuhai 「九種九牌」
                    # (9 kinds of honor or terminal tiles)
                    if 't="64"' in message:
                        self._random_sleep(1, 2)
                        # TODO aim for kokushi
                        self._send_message('<N type="9" />')
                        continue

                    drawn_tile = self.decoder.parse_tile(message)

                    logger.info('Drawn tile: {}'.format(TilesConverter.to_one_line_string([drawn_tile])))

                    kan_type = self.player.should_call_kan(drawn_tile, False, main_player.in_riichi)
                    if kan_type:
                        self._random_sleep(1, 2)

                        if kan_type == Meld.CHANKAN:
                            meld_type = 5
                            logger.info('We upgraded pon to kan!')
                        else:
                            meld_type = 4
                            logger.info('We called a closed kan set!')

                        self._send_message('<N type="{}" hai="{}" />'.format(meld_type, drawn_tile))

                        continue

                    if not main_player.in_riichi:
                        self.player.draw_tile(drawn_tile)
                        discarded_tile = self.player.discard_tile()
                        can_call_riichi = main_player.can_call_riichi()

                        # let's call riichi
                        if can_call_riichi:
                            self._random_sleep(1, 2)
                            self._send_message('<REACH hai="{}" />'.format(discarded_tile))
                            main_player.in_riichi = True
                    else:
                        # we had to add it to discards, to calculate remaining tiles correctly
                        discarded_tile = drawn_tile
                        self.table.add_discarded_tile(0, discarded_tile, True)

                    # tenhou format: <D p="133" />
                    self._send_message('<D p="{}"/>'.format(discarded_tile))
                    logger.info('Discard: {}'.format(TilesConverter.to_one_line_string([discarded_tile])))

                    logger.info('Remaining tiles: {}'.format(self.table.count_of_remaining_tiles))

                # new dora indicator after kan
                if '<DORA' in message:
                    tile = self.decoder.parse_dora_indicator(message)
                    self.table.add_dora_indicator(tile)
                    logger.info('New dora indicator: {}'.format(TilesConverter.to_one_line_string([tile])))

                if '<REACH' in message and 'step="1"' in message:
                    who_called_riichi = self.decoder.parse_who_called_riichi(message)
                    self.table.add_called_riichi(who_called_riichi)
                    logger.info('Riichi called by {} player'.format(who_called_riichi))

                # the end of round
                if '<AGARI' in message or '<RYUUKYOKU' in message:
                    self._random_sleep(1, 2)
                    self._send_message('<NEXTREADY />')

                # set was called
                if self.decoder.is_opened_set_message(message):
                    meld = self.decoder.parse_meld(message)
                    self.table.add_called_meld(meld.who, meld)
                    logger.info('Meld: {} by {}'.format(meld, meld.who))

                    # tenhou confirmed that we called a meld
                    # we had to do discard after this
                    if meld.who == 0:
                        if meld.type != Meld.KAN and meld.type != Meld.CHANKAN:
                            discarded_tile = self.player.discard_tile(tile_to_discard)

                            self.player.tiles.append(meld_tile)
                            self._send_message('<D p="{}"/>'.format(discarded_tile))

                win_suggestions = [
                    't="8"', 't="9"', 't="10"', 't="11"', 't="12"', 't="13"', 't="15"'
                ]
                # we win by other player's discard
                if any(i in message for i in win_suggestions):
                    # enemy called shouminkan and we can win there
                    if self.decoder.is_opened_set_message(message):
                        meld = self.decoder.parse_meld(message)
                        tile = meld.called_tile
                        enemy_seat = meld.who
                    else:
                        tile = self.decoder.parse_tile(message)
                        enemy_seat = self.decoder.get_enemy_seat(message)

                    self._random_sleep(1, 2)

                    if main_player.should_call_win(tile, enemy_seat):
                        self._send_message('<N type="6" />')
                    else:
                        self._send_message('<N />')

                if self.decoder.is_discarded_tile_message(message):
                    tile = self.decoder.parse_tile(message)

                    # <e21/> - is tsumogiri
                    # <E21/> - discard from the hand
                    if_tsumogiri = message[1].islower()
                    player_seat = self.decoder.get_enemy_seat(message)

                    # open hand suggestions
                    if 't=' in message:
                        # Possible t="" suggestions
                        # 1 pon
                        # 2 kan (it is a closed kan and can be send only to the self draw)
                        # 3 pon + kan
                        # 4 chi
                        # 5 pon + chi
                        # 7 pon + kan + chi

                        # should we call a kan?
                        if 't="3"' in message or 't="7"' in message:
                            if self.player.should_call_kan(tile, True):
                                self._random_sleep(1, 2)

                                # 2 is open kan
                                self._send_message('<N type="2" />')
                                logger.info('We called an open kan set!')
                                continue

                        # player with "g" discard is always our kamicha
                        is_kamicha_discard = False
                        if message[1].lower() == 'g':
                            is_kamicha_discard = True

                        meld, tile_to_discard = self.player.try_to_call_meld(tile, is_kamicha_discard)
                        if meld:
                            self._random_sleep(1, 2)

                            meld_tile = tile

                            # 1 is pon
                            meld_type = '1'
                            if meld.type == Meld.CHI:
                                # yeah it is 3, not 4
                                # because of tenhou protocol
                                meld_type = '3'

                            tiles = meld.tiles
                            tiles.remove(meld_tile)

                            # try to call a meld
                            self._send_message('<N type="{}" hai0="{}" hai1="{}" />'.format(
                                meld_type,
                                tiles[0],
                                tiles[1]
                            ))
                        # this meld will not improve our hand
                        else:
                            self._send_message('<N />')

                    self.table.add_discarded_tile(player_seat, tile, if_tsumogiri)

                if 'owari' in message:
                    values = self.decoder.parse_final_scores_and_uma(message)
                    self.table.set_players_scores(values['scores'], values['uma'])

                if '<PROF' in message:
                    self.game_is_continue = False

            # socket was closed by tenhou
            if self._count_of_empty_messages >= 5:
                logger.error('We are getting empty messages from socket. Probably socket connection was closed')
                self.end_game(False)
                return

        logger.info('Final results: {}'.format(self.table.get_players_sorted_by_scores()))

        # we need to finish the game, and only after this try to send statistics
        # if order will be different, tenhou will return 404 on log download endpoint
        self.end_game()

        # sometimes log is not available just after the game
        # let's wait one minute before the statistics update
        if settings.STAT_SERVER_URL:
            sleep(60)
            result = self.statistics.send_statistics()
            logger.info('Statistics sent: {}'.format(result))
    def _should_call_riichi_one_sided(self):
        count_tiles = self.player.ai.hand_builder.count_tiles(
            self.player.ai.waiting, TilesConverter.to_34_array(self.player.closed_hand)
        )
        waiting = self.player.ai.waiting[0]
        hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False)

        tiles = self.player.closed_hand.copy()
        closed_melds = [x for x in self.player.melds if not x.opened]
        for meld in closed_melds:
            tiles.extend(meld.tiles[:3])

        results, tiles_34 = self.player.ai.hand_builder.divide_hand(tiles, waiting)
        result = results[0]

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

        have_suji, have_kabe = self.player.ai.hand_builder.check_suji_and_kabe(closed_tiles_34, waiting)

        # what if we have yaku
        if hand_value.yaku is not None and hand_value.cost is not None:
            min_cost = hand_value.cost['main']

            # tanki honor is a good wait, let's damaten only if hand is already expensive
            if is_honor(waiting):
                if self.player.is_dealer and min_cost < 12000:
                    return True

                if not self.player.is_dealer and min_cost < 8000:
                    return True

                return False

            is_chiitoitsu = len([x for x in result if is_pair(x)]) == 7
            simplified_waiting = simplify(waiting)

            for hand_set in result:
                if waiting not in hand_set:
                    continue

                # tanki wait but not chiitoitsu
                if is_pair(hand_set) and not is_chiitoitsu:
                    # let's not riichi tanki 4, 5, 6
                    if 3 <= simplified_waiting <= 5:
                        return False

                    # don't riichi tanki wait on 1, 2, 3, 7, 8, 9 if it's only 1 tile
                    if count_tiles == 1:
                        return False

                    # don't riichi 2378 tanki if hand has good value
                    if simplified_waiting != 0 and simplified_waiting != 8:
                        if self.player.is_dealer and min_cost >= 7700:
                            return False

                        if not self.player.is_dealer and min_cost >= 5200:
                            return False

                    # only riichi if we have suji-trab or there is kabe
                    if not have_suji and not have_kabe:
                        return False

                    return True

                # tanki wait with chiitoitsu
                if is_pair(hand_set) and is_chiitoitsu:
                    # chiitoitsu on last suit tile is no the best
                    if count_tiles == 1:
                        return False

                    # only riichi if we have suji-trab or there is kabe
                    if not have_suji and not have_kabe:
                        return False

                    return True

                # 1-sided wait means kanchan or penchan
                if is_chi(hand_set):
                    # let's not riichi kanchan on 4, 5, 6
                    if 3 <= simplified_waiting <= 5:
                        return False

                    # now checking waiting for 2, 3, 7, 8
                    # if we only have 1 tile to wait for, let's damaten
                    if count_tiles == 1:
                        return False

                    # if we have 2 tiles to wait for and hand cost is good without riichi,
                    # let's damaten
                    if count_tiles == 2:
                        if self.player.is_dealer and min_cost >= 7700:
                            return False

                        if not self.player.is_dealer and min_cost >= 5200:
                            return False

                    # only riichi if we have suji-trab or there is kabe
                    if not have_suji and not have_kabe:
                        return False

                    return True

        # what if we don't have yaku
        # our tanki wait is good, let's riichi
        if is_honor(waiting):
            return True

        simplified_waiting = simplify(waiting)

        for hand_set in result:
            if waiting not in hand_set:
                continue

            if is_pair(hand_set):
                # let's not riichi tanki wait without suji-trap or kabe
                if not have_suji and not have_kabe:
                    return False

                # let's not riichi tanki on last suit tile if it's early
                if count_tiles == 1 and self.player.round_step < 6:
                    return False

                # let's not riichi tanki 4, 5, 6 if it's early
                if 3 <= simplified_waiting <= 5 and self.player.round_step < 6:
                    return False

            # 1-sided wait means kanchan or penchan
            if is_chi(hand_set):
                # let's only riichi this bad wait if
                # it has all 4 tiles available or it
                # it's not too early
                if 4 <= simplified_waiting <= 6:
                    return count_tiles == 4 or self.player.round_step >= 6

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

        player_draw = draw_tags[self.player_position]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            print('Discard: {}'.format(TilesConverter.to_one_line_string([tile])))
    def should_call_kan(self, tile, open_kan, from_riichi=False):
        """
        Method will decide should we call a kan,
        or upgrade pon to kan
        :param tile: 136 tile format
        :param open_kan: boolean
        :param from_riichi: boolean
        :return: kan type
        """

        # we can't call kan on the latest tile
        if self.table.count_of_remaining_tiles <= 1:
            return None

        # we don't need to add dora for other players
        if self.player.ai.in_defence:
            return None

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

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

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

        tile_34 = tile // 4
        tiles_34 = TilesConverter.to_34_array(self.player.tiles)

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

        melds_34 = copy.copy(self.player.meld_34_tiles)
        tiles = copy.copy(self.player.tiles)
        closed_hand_tiles = copy.copy(self.player.closed_hand)

        new_shanten = 0
        previous_shanten = 0
        new_waits_count = 0
        previous_waits_count = 0

        # let's check can we upgrade opened pon to the kan
        pon_melds = [x for x in self.player.meld_34_tiles if is_pon(x)]
        has_shouminkan_candidate = False
        for meld in pon_melds:
            # tile is equal to our already opened pon
            if tile_34 in meld:
                has_shouminkan_candidate = True

                tiles.append(tile)
                closed_hand_tiles.append(tile)

                previous_shanten, previous_waits_count = self._calculate_shanten_for_kan(
                    tiles,
                    closed_hand_tiles,
                    self.player.melds
                )

                tiles_34 = TilesConverter.to_34_array(tiles)
                tiles_34[tile_34] -= 1

                new_waiting, new_shanten = self.hand_builder.calculate_waits(
                    tiles_34,
                    self.player.meld_34_tiles
                )
                new_waits_count = self.hand_builder.count_tiles(new_waiting, tiles_34)

        if not has_shouminkan_candidate:
            # we don't have enough tiles in the hand
            if closed_hand_34[tile_34] != 3:
                return None

            if open_kan or from_riichi:
                # this 4 tiles can only be used in kan, no other options
                previous_waiting, previous_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34)
                previous_waits_count = self.hand_builder.count_tiles(previous_waiting, closed_hand_34)
            else:
                tiles.append(tile)
                closed_hand_tiles.append(tile)

                previous_shanten, previous_waits_count = self._calculate_shanten_for_kan(
                    tiles,
                    closed_hand_tiles,
                    self.player.melds
                )

            # shanten calculator doesn't like working with kans, so we pretend it's a pon
            melds_34 += [[tile_34, tile_34, tile_34]]
            new_waiting, new_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34)

            closed_hand_34[tile_34] = 4
            new_waits_count = self.hand_builder.count_tiles(new_waiting, closed_hand_34)

        # it is possible that we don't have results here
        # when we are in agari state (but without yaku)
        if previous_shanten is None:
            return None

        # it is not possible to reduce number of shanten by calling a kan
        assert new_shanten >= previous_shanten

        # if shanten number is the same, we should only call kan if ukeire didn't become worse
        if new_shanten == previous_shanten:
            # we cannot improve ukeire by calling kan (not considering the tile we drew from the dead wall)
            assert new_waits_count <= previous_waits_count

            if new_waits_count == previous_waits_count:
                return has_shouminkan_candidate and Meld.CHANKAN or Meld.KAN

        return None