Exemplo n.º 1
0
    def test_is_kokushi_musou_agari(self):
        agari = Agari()

        tiles = self._string_to_34_array(sou="19",
                                         pin="19",
                                         man="199",
                                         honors="1234567")
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou="19",
                                         pin="19",
                                         man="19",
                                         honors="11234567")
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou="19",
                                         pin="19",
                                         man="19",
                                         honors="12345677")
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou="129",
                                         pin="19",
                                         man="19",
                                         honors="1234567")
        self.assertFalse(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou="19",
                                         pin="19",
                                         man="19",
                                         honors="11134567")
        self.assertFalse(agari.is_agari(tiles))
Exemplo n.º 2
0
    def test_is_kokushi_musou_agari(self):
        agari = Agari()

        tiles = self._string_to_34_array(sou='19',
                                         pin='19',
                                         man='199',
                                         honors='1234567')
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou='19',
                                         pin='19',
                                         man='19',
                                         honors='11234567')
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou='19',
                                         pin='19',
                                         man='19',
                                         honors='12345677')
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou='129',
                                         pin='19',
                                         man='19',
                                         honors='1234567')
        self.assertFalse(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou='19',
                                         pin='19',
                                         man='19',
                                         honors='11134567')
        self.assertFalse(agari.is_agari(tiles))
Exemplo n.º 3
0
    def test_is_not_agari(self):
        agari = Agari()

        tiles = self._string_to_34_array(sou='123456789', pin='12345')
        self.assertFalse(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou='111222444', pin='11145')
        self.assertFalse(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou='11122233356888')
        self.assertFalse(agari.is_agari(tiles))
Exemplo n.º 4
0
    def test_is_not_agari(self):
        agari = Agari()

        tiles = self._string_to_34_array(sou="123456789", pin="12345")
        self.assertFalse(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou="111222444", pin="11145")
        self.assertFalse(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou="11122233356888")
        self.assertFalse(agari.is_agari(tiles))
Exemplo n.º 5
0
    def test_is_chitoitsu_agari(self):
        agari = Agari()

        tiles = self._string_to_34_array(sou='1133557799', pin='1199')
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou='2244',
                                         pin='1199',
                                         man='11',
                                         honors='2277')
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(man='11223344556677')
        self.assertTrue(agari.is_agari(tiles))
Exemplo n.º 6
0
    def test_is_chitoitsu_agari(self):
        agari = Agari()

        tiles = self._string_to_34_array(sou="1133557799", pin="1199")
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou="2244",
                                         pin="1199",
                                         man="11",
                                         honors="2277")
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(man="11223344556677")
        self.assertTrue(agari.is_agari(tiles))
Exemplo n.º 7
0
    def test_is_agari_and_open_hand(self):
        agari = Agari()

        tiles = self._string_to_34_array(sou="23455567", pin="222", man="345")
        melds = [
            self._string_to_open_34_set(man="345"),
            self._string_to_open_34_set(sou="555"),
        ]
        self.assertFalse(agari.is_agari(tiles, melds))
Exemplo n.º 8
0
    def test_is_agari_and_open_hand(self):
        agari = Agari()

        tiles = self._string_to_34_array(sou='23455567', pin='222', man='345')
        melds = [
            self._string_to_open_34_set(man='345'),
            self._string_to_open_34_set(sou='555'),
        ]
        self.assertFalse(agari.is_agari(tiles, melds))
Exemplo n.º 9
0
    def test_is_agari(self):
        agari = Agari()

        tiles = self._string_to_34_array(sou='123456789', pin='123', man='33')
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou='123456789', pin='11123')
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou='123456789', honors='11777')
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou='12345556778899')
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou='11123456788999')
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou='233334',
                                         pin='789',
                                         man='345',
                                         honors='55')
        self.assertTrue(agari.is_agari(tiles))
Exemplo n.º 10
0
    def test_is_agari(self):
        agari = Agari()

        tiles = self._string_to_34_array(sou="123456789", pin="123", man="33")
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou="123456789", pin="11123")
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou="123456789", honors="11777")
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou="12345556778899")
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou="11123456788999")
        self.assertTrue(agari.is_agari(tiles))

        tiles = self._string_to_34_array(sou="233334",
                                         pin="789",
                                         man="345",
                                         honors="55")
        self.assertTrue(agari.is_agari(tiles))
Exemplo n.º 11
0
    def estimate_hand_value(self,
                            tiles,
                            win_tile,
                            melds=None,
                            dora_indicators=None,
                            config=None):
        """
        :param tiles: array with 14 tiles in 136-tile format
        :param win_tile: 136 format tile that caused win (ron or tsumo)
        :param melds: array with Meld objects
        :param dora_indicators: array of tiles in 136-tile format
        :param config: HandConfig object
        :return: HandResponse object
        """

        if not melds:
            melds = []

        if not dora_indicators:
            dora_indicators = []

        self.config = config or HandConfig()

        agari = Agari()
        hand_yaku = []
        scores_calculator = ScoresCalculator()
        tiles_34 = TilesConverter.to_34_array(tiles)
        divider = HandDivider()
        fu_calculator = FuCalculator()

        opened_melds = [x.tiles_34 for x in melds if x.opened]
        all_melds = [x.tiles_34 for x in melds]
        is_open_hand = len(opened_melds) > 0

        # special situation
        if self.config.is_nagashi_mangan:
            hand_yaku.append(self.config.yaku.nagashi_mangan)
            fu = 30
            han = self.config.yaku.nagashi_mangan.han_closed
            cost = scores_calculator.calculate_scores(han, fu, self.config,
                                                      False)
            return HandResponse(cost, han, fu, hand_yaku)

        if win_tile not in tiles:
            return HandResponse(error="Win tile not in the hand")

        if self.config.is_riichi and is_open_hand:
            return HandResponse(
                error="Riichi can't be declared with open hand")

        if self.config.is_ippatsu and is_open_hand:
            return HandResponse(
                error="Ippatsu can't be declared with open hand")

        if self.config.is_ippatsu and not self.config.is_riichi and not self.config.is_daburu_riichi:
            return HandResponse(
                error="Ippatsu can't be declared without riichi")

        if not agari.is_agari(tiles_34, all_melds):
            return HandResponse(error='Hand is not winning')

        if not self.config.options.has_double_yakuman:
            self.config.yaku.daburu_kokushi.han_closed = 13
            self.config.yaku.suuankou_tanki.han_closed = 13
            self.config.yaku.daburu_chuuren_poutou.han_closed = 13
            self.config.yaku.daisuushi.han_closed = 13
            self.config.yaku.daisuushi.han_open = 13

        hand_options = divider.divide_hand(tiles_34, melds)

        calculated_hands = []
        for hand in hand_options:
            is_chiitoitsu = self.config.yaku.chiitoitsu.is_condition_met(hand)
            valued_tiles = [
                HAKU, HATSU, CHUN, self.config.player_wind,
                self.config.round_wind
            ]

            win_groups = self._find_win_groups(win_tile, hand, opened_melds)
            for win_group in win_groups:
                cost = None
                error = None
                hand_yaku = []
                han = 0

                fu_details, fu = fu_calculator.calculate_fu(
                    hand, win_tile, win_group, self.config, valued_tiles,
                    melds)

                is_pinfu = len(
                    fu_details) == 1 and not is_chiitoitsu and not is_open_hand

                pon_sets = [x for x in hand if is_pon(x)]
                chi_sets = [x for x in hand if is_chi(x)]

                if self.config.is_tsumo:
                    if not is_open_hand:
                        hand_yaku.append(self.config.yaku.tsumo)

                if is_pinfu:
                    hand_yaku.append(self.config.yaku.pinfu)

                # let's skip hand that looks like chitoitsu, but it contains open sets
                if is_chiitoitsu and is_open_hand:
                    continue

                if is_chiitoitsu:
                    hand_yaku.append(self.config.yaku.chiitoitsu)

                is_daisharin = self.config.yaku.daisharin.is_condition_met(
                    hand, self.config.options.has_daisharin_other_suits)
                if self.config.options.has_daisharin and is_daisharin:
                    self.config.yaku.daisharin.rename(hand)
                    hand_yaku.append(self.config.yaku.daisharin)

                is_tanyao = self.config.yaku.tanyao.is_condition_met(hand)
                if is_open_hand and not self.config.options.has_open_tanyao:
                    is_tanyao = False

                if is_tanyao:
                    hand_yaku.append(self.config.yaku.tanyao)

                if self.config.is_riichi and not self.config.is_daburu_riichi:
                    hand_yaku.append(self.config.yaku.riichi)

                if self.config.is_daburu_riichi:
                    hand_yaku.append(self.config.yaku.daburu_riichi)

                if self.config.is_ippatsu:
                    hand_yaku.append(self.config.yaku.ippatsu)

                if self.config.is_rinshan:
                    hand_yaku.append(self.config.yaku.rinshan)

                if self.config.is_chankan:
                    hand_yaku.append(self.config.yaku.chankan)

                if self.config.is_haitei:
                    hand_yaku.append(self.config.yaku.haitei)

                if self.config.is_houtei:
                    hand_yaku.append(self.config.yaku.houtei)

                if self.config.is_renhou:
                    if self.config.options.renhou_as_yakuman:
                        hand_yaku.append(self.config.yaku.renhou_yakuman)
                    else:
                        hand_yaku.append(self.config.yaku.renhou)

                if self.config.is_tenhou:
                    hand_yaku.append(self.config.yaku.tenhou)

                if self.config.is_chiihou:
                    hand_yaku.append(self.config.yaku.chiihou)

                if self.config.yaku.honitsu.is_condition_met(hand):
                    hand_yaku.append(self.config.yaku.honitsu)

                if self.config.yaku.chinitsu.is_condition_met(hand):
                    hand_yaku.append(self.config.yaku.chinitsu)

                if self.config.yaku.tsuisou.is_condition_met(hand):
                    hand_yaku.append(self.config.yaku.tsuisou)

                if self.config.yaku.honroto.is_condition_met(hand):
                    hand_yaku.append(self.config.yaku.honroto)

                if self.config.yaku.chinroto.is_condition_met(hand):
                    hand_yaku.append(self.config.yaku.chinroto)

                # small optimization, try to detect yaku with chi required sets only if we have chi sets in hand
                if len(chi_sets):
                    if self.config.yaku.chanta.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.chanta)

                    if self.config.yaku.junchan.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.junchan)

                    if self.config.yaku.ittsu.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.ittsu)

                    if not is_open_hand:
                        if self.config.yaku.ryanpeiko.is_condition_met(hand):
                            hand_yaku.append(self.config.yaku.ryanpeiko)
                        elif self.config.yaku.iipeiko.is_condition_met(hand):
                            hand_yaku.append(self.config.yaku.iipeiko)

                    if self.config.yaku.sanshoku.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.sanshoku)

                # small optimization, try to detect yaku with pon required sets only if we have pon sets in hand
                if len(pon_sets):
                    if self.config.yaku.toitoi.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.toitoi)

                    if self.config.yaku.sanankou.is_condition_met(
                            hand, win_tile, melds, self.config.is_tsumo):
                        hand_yaku.append(self.config.yaku.sanankou)

                    if self.config.yaku.sanshoku_douko.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.sanshoku_douko)

                    if self.config.yaku.shosangen.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.shosangen)

                    if self.config.yaku.haku.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.haku)

                    if self.config.yaku.hatsu.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.hatsu)

                    if self.config.yaku.chun.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.chun)

                    if self.config.yaku.east.is_condition_met(
                            hand, self.config.player_wind,
                            self.config.round_wind):
                        if self.config.player_wind == EAST:
                            hand_yaku.append(self.config.yaku.yakuhai_place)

                        if self.config.round_wind == EAST:
                            hand_yaku.append(self.config.yaku.yakuhai_round)

                    if self.config.yaku.south.is_condition_met(
                            hand, self.config.player_wind,
                            self.config.round_wind):
                        if self.config.player_wind == SOUTH:
                            hand_yaku.append(self.config.yaku.yakuhai_place)

                        if self.config.round_wind == SOUTH:
                            hand_yaku.append(self.config.yaku.yakuhai_round)

                    if self.config.yaku.west.is_condition_met(
                            hand, self.config.player_wind,
                            self.config.round_wind):
                        if self.config.player_wind == WEST:
                            hand_yaku.append(self.config.yaku.yakuhai_place)

                        if self.config.round_wind == WEST:
                            hand_yaku.append(self.config.yaku.yakuhai_round)

                    if self.config.yaku.north.is_condition_met(
                            hand, self.config.player_wind,
                            self.config.round_wind):
                        if self.config.player_wind == NORTH:
                            hand_yaku.append(self.config.yaku.yakuhai_place)

                        if self.config.round_wind == NORTH:
                            hand_yaku.append(self.config.yaku.yakuhai_round)

                    if self.config.yaku.daisangen.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.daisangen)

                    if self.config.yaku.shosuushi.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.shosuushi)

                    if self.config.yaku.daisuushi.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.daisuushi)

                    if self.config.yaku.ryuisou.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.ryuisou)

                    # closed kan can't be used in chuuren_poutou
                    if not len(
                            melds
                    ) and self.config.yaku.chuuren_poutou.is_condition_met(
                            hand):
                        if tiles_34[win_tile // 4] == 2 or tiles_34[win_tile //
                                                                    4] == 4:
                            hand_yaku.append(
                                self.config.yaku.daburu_chuuren_poutou)
                        else:
                            hand_yaku.append(self.config.yaku.chuuren_poutou)

                    if not is_open_hand and self.config.yaku.suuankou.is_condition_met(
                            hand, win_tile, self.config.is_tsumo):
                        if tiles_34[win_tile // 4] == 2:
                            hand_yaku.append(self.config.yaku.suuankou_tanki)
                        else:
                            hand_yaku.append(self.config.yaku.suuankou)

                    if self.config.yaku.sankantsu.is_condition_met(
                            hand, melds):
                        hand_yaku.append(self.config.yaku.sankantsu)

                    if self.config.yaku.suukantsu.is_condition_met(
                            hand, melds):
                        hand_yaku.append(self.config.yaku.suukantsu)

                # yakuman is not connected with other yaku
                yakuman_list = [x for x in hand_yaku if x.is_yakuman]
                if yakuman_list:
                    hand_yaku = yakuman_list

                # calculate han
                for item in hand_yaku:
                    if is_open_hand and item.han_open:
                        han += item.han_open
                    else:
                        han += item.han_closed

                if han == 0:
                    error = 'There are no yaku in the hand'
                    cost = None

                # we don't need to add dora to yakuman
                if not yakuman_list:
                    tiles_for_dora = tiles[:]

                    # we had to search for dora in kan fourth tiles as well
                    for meld in melds:
                        if meld.type == Meld.KAN or meld.type == Meld.CHANKAN:
                            tiles_for_dora.append(meld.tiles[3])

                    count_of_dora = 0
                    count_of_aka_dora = 0

                    for tile in tiles_for_dora:
                        count_of_dora += plus_dora(tile, dora_indicators)

                    for tile in tiles_for_dora:
                        if is_aka_dora(tile, self.config.options.has_aka_dora):
                            count_of_aka_dora += 1

                    if count_of_dora:
                        self.config.yaku.dora.han_open = count_of_dora
                        self.config.yaku.dora.han_closed = count_of_dora
                        hand_yaku.append(self.config.yaku.dora)
                        han += count_of_dora

                    if count_of_aka_dora:
                        self.config.yaku.aka_dora.han_open = count_of_aka_dora
                        self.config.yaku.aka_dora.han_closed = count_of_aka_dora
                        hand_yaku.append(self.config.yaku.aka_dora)
                        han += count_of_aka_dora

                if not error:
                    cost = scores_calculator.calculate_scores(
                        han, fu, self.config,
                        len(yakuman_list) > 0)

                calculated_hand = {
                    'cost': cost,
                    'error': error,
                    'hand_yaku': hand_yaku,
                    'han': han,
                    'fu': fu,
                    'fu_details': fu_details
                }

                calculated_hands.append(calculated_hand)

        # exception hand
        if not is_open_hand and self.config.yaku.kokushi.is_condition_met(
                None, tiles_34):
            if tiles_34[win_tile // 4] == 2:
                hand_yaku.append(self.config.yaku.daburu_kokushi)
            else:
                hand_yaku.append(self.config.yaku.kokushi)

            if self.config.is_renhou and self.config.options.renhou_as_yakuman:
                hand_yaku.append(self.config.yaku.renhou_yakuman)

            if self.config.is_tenhou:
                hand_yaku.append(self.config.yaku.tenhou)

            if self.config.is_chiihou:
                hand_yaku.append(self.config.yaku.chiihou)

            # calculate han
            han = 0
            for item in hand_yaku:
                if is_open_hand and item.han_open:
                    han += item.han_open
                else:
                    han += item.han_closed

            fu = 0
            cost = scores_calculator.calculate_scores(han, fu, self.config,
                                                      len(hand_yaku) > 0)
            calculated_hands.append({
                'cost': cost,
                'error': None,
                'hand_yaku': hand_yaku,
                'han': han,
                'fu': fu,
                'fu_details': []
            })

        # let's use cost for most expensive hand
        calculated_hands = sorted(calculated_hands,
                                  key=lambda x: (x['han'], x['fu']),
                                  reverse=True)
        calculated_hand = calculated_hands[0]

        cost = calculated_hand['cost']
        error = calculated_hand['error']
        hand_yaku = calculated_hand['hand_yaku']
        han = calculated_hand['han']
        fu = calculated_hand['fu']
        fu_details = calculated_hand['fu_details']

        return HandResponse(cost, han, fu, hand_yaku, error, fu_details)
Exemplo n.º 12
0
    def estimate_hand_value(
        self,
        tiles,
        win_tile,
        melds=None,
        dora_indicators=None,
        config=None,
        scores_calculator_factory=ScoresCalculator,
        use_hand_divider_cache=False,
    ):
        """
        :param tiles: array with 14 tiles in 136-tile format
        :param win_tile: 136 format tile that caused win (ron or tsumo)
        :param melds: array with Meld objects
        :param dora_indicators: array of tiles in 136-tile format
        :param config: HandConfig object
        :param use_hand_divider_cache: could be useful if you are calculating a lot of menchin hands
        :return: HandResponse object
        """

        if not melds:
            melds = []

        if not dora_indicators:
            dora_indicators = []

        self.config = config or HandConfig()

        agari = Agari()
        hand_yaku = []
        scores_calculator = scores_calculator_factory()
        tiles_34 = TilesConverter.to_34_array(tiles)

        fu_calculator = FuCalculator()
        is_aotenjou = isinstance(scores_calculator, Aotenjou)

        opened_melds = [x.tiles_34 for x in melds if x.opened]
        all_melds = [x.tiles_34 for x in melds]
        is_open_hand = len(opened_melds) > 0

        # special situation
        if self.config.is_nagashi_mangan:
            hand_yaku.append(self.config.yaku.nagashi_mangan)
            fu = 30
            han = self.config.yaku.nagashi_mangan.han_closed
            cost = scores_calculator.calculate_scores(han, fu, self.config,
                                                      False)
            return HandResponse(cost, han, fu, hand_yaku)

        if win_tile not in tiles:
            return HandResponse(error=HandCalculator.ERR_NO_WINNING_TILE)

        if self.config.is_riichi and not self.config.is_daburu_riichi and is_open_hand:
            return HandResponse(error=HandCalculator.ERR_OPEN_HAND_RIICHI)

        if self.config.is_daburu_riichi and is_open_hand:
            return HandResponse(error=HandCalculator.ERR_OPEN_HAND_DABURI)

        if self.config.is_ippatsu and not self.config.is_riichi and not self.config.is_daburu_riichi:
            return HandResponse(
                error=HandCalculator.ERR_IPPATSU_WITHOUT_RIICHI)

        if self.config.is_chankan and self.config.is_tsumo:
            return HandResponse(error=HandCalculator.ERR_CHANKAN_WITH_TSUMO)

        if self.config.is_rinshan and not self.config.is_tsumo:
            return HandResponse(error=HandCalculator.ERR_RINSHAN_WITHOUT_TSUMO)

        if self.config.is_haitei and not self.config.is_tsumo:
            return HandResponse(error=HandCalculator.ERR_HAITEI_WITHOUT_TSUMO)

        if self.config.is_houtei and self.config.is_tsumo:
            return HandResponse(error=HandCalculator.ERR_HOUTEI_WITH_TSUMO)

        if self.config.is_haitei and self.config.is_rinshan:
            return HandResponse(error=HandCalculator.ERR_HAITEI_WITH_RINSHAN)

        if self.config.is_houtei and self.config.is_chankan:
            return HandResponse(error=HandCalculator.ERR_HOUTEI_WITH_CHANKAN)

        # raise error only when player wind is defined (and is *not* EAST)
        if self.config.is_tenhou and self.config.player_wind and not self.config.is_dealer:
            return HandResponse(error=HandCalculator.ERR_TENHOU_NOT_AS_DEALER)

        if self.config.is_tenhou and not self.config.is_tsumo:
            return HandResponse(error=HandCalculator.ERR_TENHOU_WITHOUT_TSUMO)

        if self.config.is_tenhou and melds:
            return HandResponse(error=HandCalculator.ERR_TENHOU_WITH_MELD)

        # raise error only when player wind is defined (and is EAST)
        if self.config.is_chiihou and self.config.player_wind and self.config.is_dealer:
            return HandResponse(error=HandCalculator.ERR_CHIIHOU_AS_DEALER)

        if self.config.is_chiihou and not self.config.is_tsumo:
            return HandResponse(error=HandCalculator.ERR_CHIIHOU_WITHOUT_TSUMO)

        if self.config.is_chiihou and melds:
            return HandResponse(error=HandCalculator.ERR_CHIIHOU_WITH_MELD)

        # raise error only when player wind is defined (and is EAST)
        if self.config.is_renhou and self.config.player_wind and self.config.is_dealer:
            return HandResponse(error=HandCalculator.ERR_RENHOU_AS_DEALER)

        if self.config.is_renhou and self.config.is_tsumo:
            return HandResponse(error=HandCalculator.ERR_RENHOU_WITH_TSUMO)

        if self.config.is_renhou and melds:
            return HandResponse(error=HandCalculator.ERR_RENHOU_WITH_MELD)

        if not agari.is_agari(tiles_34, all_melds):
            return HandResponse(error=HandCalculator.ERR_HAND_NOT_WINNING)

        if not self.config.options.has_double_yakuman:
            self.config.yaku.daburu_kokushi.han_closed = 13
            self.config.yaku.suuankou_tanki.han_closed = 13
            self.config.yaku.daburu_chuuren_poutou.han_closed = 13
            self.config.yaku.daisuushi.han_closed = 13
            self.config.yaku.daisuushi.han_open = 13

        hand_options = self.divider.divide_hand(
            tiles_34, melds, use_cache=use_hand_divider_cache)

        calculated_hands = []
        for hand in hand_options:
            is_chiitoitsu = self.config.yaku.chiitoitsu.is_condition_met(hand)
            valued_tiles = [
                HAKU, HATSU, CHUN, self.config.player_wind,
                self.config.round_wind
            ]

            win_groups = self._find_win_groups(win_tile, hand, opened_melds)
            for win_group in win_groups:
                cost = None
                error = None
                hand_yaku = []
                han = 0

                fu_details, fu = fu_calculator.calculate_fu(
                    hand, win_tile, win_group, self.config, valued_tiles,
                    melds)

                is_pinfu = len(
                    fu_details) == 1 and not is_chiitoitsu and not is_open_hand

                pon_sets = [x for x in hand if is_pon(x)]
                kan_sets = [x for x in hand if is_kan(x)]
                chi_sets = [x for x in hand if is_chi(x)]

                if self.config.is_tsumo:
                    if not is_open_hand:
                        hand_yaku.append(self.config.yaku.tsumo)

                if is_pinfu:
                    hand_yaku.append(self.config.yaku.pinfu)

                # let's skip hand that looks like chitoitsu, but it contains open sets
                if is_chiitoitsu and is_open_hand:
                    continue

                if is_chiitoitsu:
                    hand_yaku.append(self.config.yaku.chiitoitsu)

                is_daisharin = self.config.yaku.daisharin.is_condition_met(
                    hand, self.config.options.has_daisharin_other_suits)
                if self.config.options.has_daisharin and is_daisharin:
                    self.config.yaku.daisharin.rename(hand)
                    hand_yaku.append(self.config.yaku.daisharin)

                if self.config.options.has_daichisei and self.config.yaku.daichisei.is_condition_met(
                        hand):
                    hand_yaku.append(self.config.yaku.daichisei)

                is_tanyao = self.config.yaku.tanyao.is_condition_met(hand)
                if is_open_hand and not self.config.options.has_open_tanyao:
                    is_tanyao = False

                if is_tanyao:
                    hand_yaku.append(self.config.yaku.tanyao)

                if self.config.is_riichi and not self.config.is_daburu_riichi:
                    if self.config.is_open_riichi:
                        hand_yaku.append(self.config.yaku.open_riichi)
                    else:
                        hand_yaku.append(self.config.yaku.riichi)

                if self.config.is_daburu_riichi:
                    if self.config.is_open_riichi:
                        hand_yaku.append(self.config.yaku.daburu_open_riichi)
                    else:
                        hand_yaku.append(self.config.yaku.daburu_riichi)

                if (not self.config.is_tsumo
                        and self.config.options.has_sashikomi_yakuman and
                    ((self.config.yaku.daburu_open_riichi in hand_yaku) or
                     (self.config.yaku.open_riichi in hand_yaku))):
                    hand_yaku.append(self.config.yaku.sashikomi)

                if self.config.is_ippatsu:
                    hand_yaku.append(self.config.yaku.ippatsu)

                if self.config.is_rinshan:
                    hand_yaku.append(self.config.yaku.rinshan)

                if self.config.is_chankan:
                    hand_yaku.append(self.config.yaku.chankan)

                if self.config.is_haitei:
                    hand_yaku.append(self.config.yaku.haitei)

                if self.config.is_houtei:
                    hand_yaku.append(self.config.yaku.houtei)

                if self.config.is_renhou:
                    if self.config.options.renhou_as_yakuman:
                        hand_yaku.append(self.config.yaku.renhou_yakuman)
                    else:
                        hand_yaku.append(self.config.yaku.renhou)

                if self.config.is_tenhou:
                    hand_yaku.append(self.config.yaku.tenhou)

                if self.config.is_chiihou:
                    hand_yaku.append(self.config.yaku.chiihou)

                if self.config.yaku.honitsu.is_condition_met(hand):
                    hand_yaku.append(self.config.yaku.honitsu)

                if self.config.yaku.chinitsu.is_condition_met(hand):
                    hand_yaku.append(self.config.yaku.chinitsu)

                if self.config.yaku.tsuisou.is_condition_met(hand):
                    hand_yaku.append(self.config.yaku.tsuisou)

                if self.config.yaku.honroto.is_condition_met(hand):
                    hand_yaku.append(self.config.yaku.honroto)

                if self.config.yaku.chinroto.is_condition_met(hand):
                    hand_yaku.append(self.config.yaku.chinroto)

                if self.config.yaku.ryuisou.is_condition_met(hand):
                    hand_yaku.append(self.config.yaku.ryuisou)

                if self.config.paarenchan > 0 and not self.config.options.paarenchan_needs_yaku:
                    # if no yaku is even needed to win on paarenchan and it is paarenchan condition, just add paarenchan
                    self.config.yaku.paarenchan.set_paarenchan_count(
                        self.config.paarenchan)
                    hand_yaku.append(self.config.yaku.paarenchan)

                # small optimization, try to detect yaku with chi required sets only if we have chi sets in hand
                if len(chi_sets):
                    if self.config.yaku.chantai.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.chantai)

                    if self.config.yaku.junchan.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.junchan)

                    if self.config.yaku.ittsu.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.ittsu)

                    if not is_open_hand:
                        if self.config.yaku.ryanpeiko.is_condition_met(hand):
                            hand_yaku.append(self.config.yaku.ryanpeiko)
                        elif self.config.yaku.iipeiko.is_condition_met(hand):
                            hand_yaku.append(self.config.yaku.iipeiko)

                    if self.config.yaku.sanshoku.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.sanshoku)

                # small optimization, try to detect yaku with pon required sets only if we have pon sets in hand
                if len(pon_sets) or len(kan_sets):
                    if self.config.yaku.toitoi.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.toitoi)

                    if self.config.yaku.sanankou.is_condition_met(
                            hand, win_tile, melds, self.config.is_tsumo):
                        hand_yaku.append(self.config.yaku.sanankou)

                    if self.config.yaku.sanshoku_douko.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.sanshoku_douko)

                    if self.config.yaku.shosangen.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.shosangen)

                    if self.config.yaku.haku.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.haku)

                    if self.config.yaku.hatsu.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.hatsu)

                    if self.config.yaku.chun.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.chun)

                    if self.config.yaku.east.is_condition_met(
                            hand, self.config.player_wind,
                            self.config.round_wind):
                        if self.config.player_wind == EAST:
                            hand_yaku.append(self.config.yaku.yakuhai_place)

                        if self.config.round_wind == EAST:
                            hand_yaku.append(self.config.yaku.yakuhai_round)

                    if self.config.yaku.south.is_condition_met(
                            hand, self.config.player_wind,
                            self.config.round_wind):
                        if self.config.player_wind == SOUTH:
                            hand_yaku.append(self.config.yaku.yakuhai_place)

                        if self.config.round_wind == SOUTH:
                            hand_yaku.append(self.config.yaku.yakuhai_round)

                    if self.config.yaku.west.is_condition_met(
                            hand, self.config.player_wind,
                            self.config.round_wind):
                        if self.config.player_wind == WEST:
                            hand_yaku.append(self.config.yaku.yakuhai_place)

                        if self.config.round_wind == WEST:
                            hand_yaku.append(self.config.yaku.yakuhai_round)

                    if self.config.yaku.north.is_condition_met(
                            hand, self.config.player_wind,
                            self.config.round_wind):
                        if self.config.player_wind == NORTH:
                            hand_yaku.append(self.config.yaku.yakuhai_place)

                        if self.config.round_wind == NORTH:
                            hand_yaku.append(self.config.yaku.yakuhai_round)

                    if self.config.yaku.daisangen.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.daisangen)

                    if self.config.yaku.shosuushi.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.shosuushi)

                    if self.config.yaku.daisuushi.is_condition_met(hand):
                        hand_yaku.append(self.config.yaku.daisuushi)

                    # closed kan can't be used in chuuren_poutou
                    if not len(
                            melds
                    ) and self.config.yaku.chuuren_poutou.is_condition_met(
                            hand):
                        if tiles_34[win_tile // 4] == 2 or tiles_34[win_tile //
                                                                    4] == 4:
                            hand_yaku.append(
                                self.config.yaku.daburu_chuuren_poutou)
                        else:
                            hand_yaku.append(self.config.yaku.chuuren_poutou)

                    if not is_open_hand and self.config.yaku.suuankou.is_condition_met(
                            hand, win_tile, self.config.is_tsumo):
                        if tiles_34[win_tile // 4] == 2:
                            hand_yaku.append(self.config.yaku.suuankou_tanki)
                        else:
                            hand_yaku.append(self.config.yaku.suuankou)

                    if self.config.yaku.sankantsu.is_condition_met(
                            hand, melds):
                        hand_yaku.append(self.config.yaku.sankantsu)

                    if self.config.yaku.suukantsu.is_condition_met(
                            hand, melds):
                        hand_yaku.append(self.config.yaku.suukantsu)

                if self.config.paarenchan > 0 and self.config.options.paarenchan_needs_yaku and len(
                        hand_yaku) > 0:
                    # we waited until here to add paarenchan yakuman only if there is any other yaku
                    self.config.yaku.paarenchan.set_paarenchan_count(
                        self.config.paarenchan)
                    hand_yaku.append(self.config.yaku.paarenchan)

                # yakuman is not connected with other yaku
                yakuman_list = [x for x in hand_yaku if x.is_yakuman]
                if yakuman_list:
                    if not is_aotenjou:
                        hand_yaku = yakuman_list
                    else:
                        scores_calculator.aotenjou_filter_yaku(
                            hand_yaku, self.config)
                        yakuman_list = []

                # calculate han
                for item in hand_yaku:
                    if is_open_hand and item.han_open:
                        han += item.han_open
                    else:
                        han += item.han_closed

                if han == 0:
                    error = HandCalculator.ERR_NO_YAKU
                    cost = None

                # we don't need to add dora to yakuman
                if not yakuman_list:
                    tiles_for_dora = tiles[:]

                    count_of_dora = 0
                    count_of_aka_dora = 0

                    for tile in tiles_for_dora:
                        count_of_dora += plus_dora(tile, dora_indicators)

                    for tile in tiles_for_dora:
                        if is_aka_dora(tile, self.config.options.has_aka_dora):
                            count_of_aka_dora += 1

                    if count_of_dora:
                        self.config.yaku.dora.han_open = count_of_dora
                        self.config.yaku.dora.han_closed = count_of_dora
                        hand_yaku.append(self.config.yaku.dora)
                        han += count_of_dora

                    if count_of_aka_dora:
                        self.config.yaku.aka_dora.han_open = count_of_aka_dora
                        self.config.yaku.aka_dora.han_closed = count_of_aka_dora
                        hand_yaku.append(self.config.yaku.aka_dora)
                        han += count_of_aka_dora

                if not is_aotenjou and (
                        self.config.options.limit_to_sextuple_yakuman
                        and han > 78):
                    han = 78

                if fu == 0 and is_aotenjou:
                    fu = 40

                if not error:
                    cost = scores_calculator.calculate_scores(
                        han, fu, self.config,
                        len(yakuman_list) > 0)

                calculated_hand = {
                    "cost": cost,
                    "error": error,
                    "hand_yaku": hand_yaku,
                    "han": han,
                    "fu": fu,
                    "fu_details": fu_details,
                }

                calculated_hands.append(calculated_hand)

        # exception hand
        if not is_open_hand and self.config.yaku.kokushi.is_condition_met(
                None, tiles_34):
            if tiles_34[win_tile // 4] == 2:
                hand_yaku.append(self.config.yaku.daburu_kokushi)
            else:
                hand_yaku.append(self.config.yaku.kokushi)

            if not self.config.is_tsumo and self.config.options.has_sashikomi_yakuman:
                if self.config.is_riichi and not self.config.is_daburu_riichi:
                    if self.config.is_open_riichi:
                        hand_yaku.append(self.config.yaku.sashikomi)

                if self.config.is_daburu_riichi:
                    if self.config.is_open_riichi:
                        hand_yaku.append(self.config.yaku.sashikomi)

            if self.config.is_renhou and self.config.options.renhou_as_yakuman:
                hand_yaku.append(self.config.yaku.renhou_yakuman)

            if self.config.is_tenhou:
                hand_yaku.append(self.config.yaku.tenhou)

            if self.config.is_chiihou:
                hand_yaku.append(self.config.yaku.chiihou)

            if self.config.paarenchan > 0:
                self.config.yaku.paarenchan.set_paarenchan_count(
                    self.config.paarenchan)
                hand_yaku.append(self.config.yaku.paarenchan)

            # calculate han
            han = 0
            for item in hand_yaku:
                if is_open_hand and item.han_open:
                    han += item.han_open
                else:
                    han += item.han_closed

            fu = 0
            if is_aotenjou:
                if self.config.is_tsumo:
                    fu = 30
                else:
                    fu = 40

                tiles_for_dora = tiles[:]

                count_of_dora = 0
                count_of_aka_dora = 0

                for tile in tiles_for_dora:
                    count_of_dora += plus_dora(tile, dora_indicators)

                for tile in tiles_for_dora:
                    if is_aka_dora(tile, self.config.options.has_aka_dora):
                        count_of_aka_dora += 1

                if count_of_dora:
                    self.config.yaku.dora.han_open = count_of_dora
                    self.config.yaku.dora.han_closed = count_of_dora
                    hand_yaku.append(self.config.yaku.dora)
                    han += count_of_dora

                if count_of_aka_dora:
                    self.config.yaku.aka_dora.han_open = count_of_aka_dora
                    self.config.yaku.aka_dora.han_closed = count_of_aka_dora
                    hand_yaku.append(self.config.yaku.aka_dora)
                    han += count_of_aka_dora

            cost = scores_calculator.calculate_scores(han, fu, self.config,
                                                      len(hand_yaku) > 0)
            calculated_hands.append({
                "cost": cost,
                "error": None,
                "hand_yaku": hand_yaku,
                "han": han,
                "fu": fu,
                "fu_details": []
            })

        # let's use cost for most expensive hand
        calculated_hands = sorted(calculated_hands,
                                  key=lambda x: (x["han"], x["fu"]),
                                  reverse=True)
        calculated_hand = calculated_hands[0]

        cost = calculated_hand["cost"]
        error = calculated_hand["error"]
        hand_yaku = calculated_hand["hand_yaku"]
        han = calculated_hand["han"]
        fu = calculated_hand["fu"]
        fu_details = calculated_hand["fu_details"]

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

    shanten = None
    agari = None
    shdict = {}

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

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

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

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

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

        if discard_tile is not None:
            return discard_tile

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

        results = []

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

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

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

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

        (minshanten, discard_34) = min(results)

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

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

        (h, discard_34) = min(results2)

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

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

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

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

        # print(hidden_34)

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

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

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

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

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

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

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

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

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

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

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

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

        return outs

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

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

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

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

            # don't call open kan if not waiting for win
            if not self.player.in_tempai:
                return None

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

        # upgrade open pon to kan if possible
        if pon_melds:
            for meld in pon_melds:
                if tile_34 in meld:
                    return Meld.CHANKAN

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

        if closed_hand_34[tile_34] == count_of_needed_tiles:
            if not open_kan:
                # to correctly count shanten in the hand
                # we had do subtract drown tile
                tiles_34[tile_34] -= 1

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

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

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

        return None

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

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

        closed_hand = self.player.closed_hand[:]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            tiles_136 = [first_tile, second_tile, tile]

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

        return None, None

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

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

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

        results = []

        for tile in range(0, 34):

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

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

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

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

        (minshanten, discard_34) = min(results)

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

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

        (h, discard_34) = min(results2)

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

        return minshanten, discard_136
Exemplo n.º 14
0
class ImplementationAI(InterfaceAI):
    version = '0.3.2'

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

    previous_shanten = 7
    in_defence = False
    waiting = None

    current_strategy = None

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

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

        # Added for cowboy
        self.wanted_tiles_count = 0
        self.pushing = False

    def init_hand(self):
        """
        Let's decide what we will do with our hand (like open for tanyao and etc.)
        """
        self.determine_strategy()

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

        # Added for cowboy
        self.previous_shanten = 7
        self.pushing = False

    def draw_tile(self, tile):
        """
        :param tile: 136 tile format
        :return:
        """
        self.determine_strategy()

    def discard_tile(self, discard_tile):
        # we called meld and we had discard tile that we wanted to discard
        if discard_tile is not None:
            if not self.last_discard_option:
                return discard_tile

            return self.process_discard_option(self.last_discard_option, self.player.closed_hand, True)

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

        if shanten < self.previous_shanten:
            logger.info("Shanten: {}".format(shanten))
            self.previous_shanten = shanten

        selected_tile = self.process_discard_options_and_select_tile_to_discard(results, shanten)

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

            defence_results, shanten = self.calculate_outs(self.player.tiles,
                                                   self.player.closed_hand,
                                                   self.player.open_hand_34_tiles)

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

        # Process the discard option before changing the state
        card2discard = self.process_discard_option(selected_tile, self.player.closed_hand)

        # After adjusting the defence, time to update the state
        if shanten == 0 and self.player.play_state == "PREPARING" and results:  # and results for debugging
            if self.wanted_tiles_count > 4:
                self.player.set_state("PROACTIVE_GOODSHAPE")
            else:
                self.player.set_state("PROACTIVE_BADSHAPE")

        return card2discard

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

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

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

        return self.chose_tile_to_discard(results)

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

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

            tiles_34[hand_tile] -= 1

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

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

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

            tiles_34[hand_tile] += 1

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

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

        return results, shanten

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

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

        meld, discard_option = self.current_strategy.try_to_call_meld(tile, is_kamicha_discard)
        tile_to_discard = None
        if discard_option:
            self.last_discard_option = discard_option
            tile_to_discard = discard_option.tile_to_discard

        return meld, tile_to_discard

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

        old_strategy = self.current_strategy
        self.current_strategy = None

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

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

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

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

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

        return self.current_strategy and True or False

    def chose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption:
        """
        Try to find best tile to discard, based on different valuations
        """

        def sorting(x):
            # - is important for x.tiles_count
            # in that case we will discard tile that will give for us more tiles
            # to complete a hand
            return x.shanten, -x.tiles_count, x.valuation

        # util for drawing
        def get_order(t):
            # if it is honor
            if t // 9 >= 3:
                return 0
            else:
                return min((t % 9), (8 - (t % 9))) + 1

        def display_waiting(w):
            return TilesConverter.to_one_line_string([t * 4 for t in w])

        had_to_be_discarded_tiles = [x for x in results if x.had_to_be_discarded]
        if had_to_be_discarded_tiles:
            had_to_be_discarded_tiles = sorted(had_to_be_discarded_tiles, key=sorting)
            selected_tile = had_to_be_discarded_tiles[0]
        else:
            results = sorted(results, key=sorting)
            #print("Len: ", len(results))

            # init the temp_tile
            temp_tile = results[0]

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

            # let's chose most valuable tile first
            if results:
                temp_tile = results[0]
            else:
                return temp_tile

            # and let's find all tiles with same shanten
            results_with_same_shanten = [x for x in results if x.shanten == temp_tile.shanten]

            # if there are 7 pairs
            tiles_34 = TilesConverter.to_34_array(self.player.tiles)
            paired_tiles = [x for x in range(0, 34) if tiles_34[x] == 2]
            num_pairs = len(paired_tiles)

            if num_pairs == 4 and temp_tile.shanten > 1 and not self.player.in_seven_pairs and not self.player.params.get("fool_in_pairs"):
                logger.info("There are 4 pairs!")

                if len(self.player.discards) > 6:
                    logger.info("However it's too late for seven pairs.")
                    for r in results:
                        if r.tile_to_discard in paired_tiles:
                            logger.info("With hand: {}".format(TilesConverter.to_one_line_string(self.player.tiles)))
                            logger.info("Discard {}".format(display_waiting([r.tile_to_discard])))
                            return r
                else:
                    logger.info("It's early, okay to go with seven pairs.")
                    self.player.in_seven_pairs = True


            # TODO: a smart seven pairs strategy should be carried
            if self.player.in_seven_pairs and not self.player.params.get("fool_in_pairs"):
                single_tiles = [x for x in range(0,34) if tiles_34[x] in [1,3,4]]
                single_tiles.sort(key=lambda x: (self.count_tiles([x], tiles_34) >= 2, -get_order(x)))
                for s in single_tiles: # actually only #1 would be used most of the time
                    for r in results:
                        if r.tile_to_discard == s:
                            logger.info("SevenPairsStrategy:")
                            logger.info("Hand: {}".format(TilesConverter.to_one_line_string(self.player.tiles)))
                            logger.info("Discard: {}".format(display_waiting([s])))
                            return r


            # if in drawing
            if temp_tile.shanten == 0:
                print("It's a drawing hand!")
                print("Hand: {}".format(TilesConverter.to_one_line_string(self.player.tiles)))
                # assume that temp tile got the biggest waiting
                if temp_tile.tiles_count > 4:
                    print("It's a good shape, go for it.")
                else:
                    logger.info("It's a bad shape drawing hand, need some calculation.")
                    logger.info("Possible choices: {}".format(TilesConverter.to_one_line_string([x.tile_to_discard*4 for x in results_with_same_shanten])))
                    possible_choices = [(temp_tile, 99)]
                    for r in results_with_same_shanten:
                        print("\nCut:", display_waiting([r.tile_to_discard]))
                        print("Waiting:", display_waiting(r.waiting))
                        print("Order:", [get_order(t) for t in r.waiting])
                        print("Outs:", r.tiles_count)
                        if r.tiles_count == 0:
                            print("It's an impossible drawing.")
                            continue
                        if len(r.waiting) == 1:
                            print("It's an 1 out drawing.")
                            possible_choices.append((r, get_order(r.waiting[0])))
                        else:
                            print("It's a multiple out drawing.")
                            r.waiting.sort(key=get_order)
                            possible_choices.append((r, get_order(r.waiting[0])))
                    possible_choices.sort(key=lambda x: (x[1], -x[0].tiles_count))
                    final_choice = possible_choices[0][0]
                    logger.info("Choice: {} {} {}".format(display_waiting([final_choice.tile_to_discard]), "with waiting", display_waiting(final_choice.waiting)))

                    return final_choice

            # if not in drawing or in drawing with good shape
            possible_options = [temp_tile]
            for discard_option in results_with_same_shanten:
                # there is no sense to check already chosen tile
                if discard_option.tile_to_discard == temp_tile.tile_to_discard:
                    continue

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

                # let's check all other tiles with same shanten
                # maybe we can find tiles that have almost same tiles count number
                # Cowboy: +-2 is a big difference, but +-1 is not
                diff = 1
                if self.player.params.get("big_diff"):
                    diff = 2
                if temp_tile.tiles_count - diff < discard_option.tiles_count < temp_tile.tiles_count + diff:
                    possible_options.append(discard_option)

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

            if selected_tile.shanten == 0:
                print("\nChoice:", display_waiting([selected_tile.tile_to_discard]), "with waiting",
                      display_waiting(selected_tile.waiting))

        return selected_tile

    def process_discard_option(self, discard_option, closed_hand, force_discard=False):
        self.waiting = discard_option.waiting
        self.wanted_tiles_count = discard_option.tiles_count
        self.player.ai.previous_shanten = discard_option.shanten
        self.player.in_tempai = self.player.ai.previous_shanten == 0

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

        last_draw_34 = self.player.last_draw  and self.player.last_draw // 4 or None
        if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard:
            return self.player.last_draw
        else:
            return discard_option.find_tile_in_hand(closed_hand)

    def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False):
        """
        :param win_tile: 34 tile format
        :param tiles:
        :param call_riichi:
        :return:
        """
        win_tile *= 4

        # we don't need to think, that our waiting is aka dora
        if win_tile in AKA_DORA_LIST:
            win_tile += 1

        if not tiles:
            tiles = self.player.tiles

        tiles += [win_tile]

        config = HandConfig(
            is_riichi=call_riichi,
            player_wind=self.player.player_wind,
            round_wind=self.player.table.round_wind,
            has_aka_dora=self.player.table.has_aka_dora,
            has_open_tanyao=self.player.table.has_open_tanyao
        )

        result = self.finished_hand.estimate_hand_value(tiles,
                                                        win_tile,
                                                        self.player.melds,
                                                        self.player.table.dora_indicators,
                                                        config)
        return result

    def should_call_riichi(self):
        logger.info("Can call a reach!")

        # empty waiting can be found in some cases
        if not self.waiting:
            logger.info("However it is impossible to win.")
            return False

        # In pushing state, it's better to call it
        if self.pushing:
            logger.info("Go for it! The player is in pushing state.")
            return True

        # Get the rank EV after round 3
        if self.table.round_number >= 5:  # DEBUG: set this to 0
            try:
                possible_hand_values = [self.estimate_hand_value(tile, call_riichi=True).cost["main"] for tile in self.waiting]
            except Exception as e:
                print(e)
                possible_hand_values = [2000]
            hand_value = sum(possible_hand_values) / len(possible_hand_values)
            hand_value += self.table.count_of_riichi_sticks * 1000
            if self.player.is_dealer:
                hand_value += 700  # EV for dealer combo

            lose_estimation = 6000 if self.player.is_dealer else 7000

            hand_shape = "pro_bad_shape" if self.wanted_tiles_count <= 4 else "pro_good_shape"

            rank_ev = self.defence.get_rank_ev(hand_value, lose_estimation, COUNTER_RATIO[hand_shape][len(self.player.discards)])

            logger.info('''Cowboy: Proactive reach:
            Hand value: {}    Hand shape: {}
            Is dealer: {}    Current ranking: {}
            '''.format(hand_value, hand_shape, self.player.is_dealer, self.table.get_players_sorted_by_scores()))

            logger.info("Rank EV for proactive reach: {}".format(rank_ev))

            if rank_ev < 0:
                logger.info("It's better to fold.")
                return False
            else:
                logger.info("Go for it!")
                return True

        should_attack = not self.defence.should_go_to_defence_mode()

        # For bad shape, at least 1 dora is required
        # Get count of dora
        dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles])
        # aka dora
        dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_open_tanyao)])
        if self.wanted_tiles_count <= 4 and dora_count == 0 and not self.player.is_dealer:
            should_attack = False
            logger.info("A bad shape with no dora, don't call it.")

        # # If player is on the top, no need to call reach
        # if self.player == self.player.table.get_players_sorted_by_scores()[0] and self.player.scores > 30000:
        #     should_attack = False
        #     logger.info("Player is in 1st position, no need to call reach.")

        if should_attack:
            # If we are proactive, let's set the state!
            logger.info("Go for it!")
            if self.player.play_state == "PREPARING": # If not changed in defense actions
                if self.wanted_tiles_count > 4:
                    self.player.set_state("PROACTIVE_GOODSHAPE")
                else:
                    self.player.set_state("PROACTIVE_BADSHAPE")
            return True

        else:
            logger.info("However it's better to fold.")
            return False


        # These codes are unreachable, it is fine.
        waiting = self.waiting[0]
        tiles = self.player.closed_hand + [waiting * 4]
        closed_melds = [x for x in self.player.melds if not x.opened]
        for meld in closed_melds:
            tiles.extend(meld.tiles[:3])

        tiles_34 = TilesConverter.to_34_array(tiles)

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

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

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

        return True

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

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

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

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

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

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

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

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

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

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

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

        return None

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

    def enemy_called_riichi(self, enemy_seat):
        """
        After enemy riichi we had to check will we fold or not
        it is affect open hand decisions
        :return:
        """
        #if self.defence.should_go_to_defence_mode():
        #    self.in_defence = True

        # No need to check it here

        pass

    @property
    def enemy_players(self):
        """
        Return list of players except our bot
        """
        return self.player.table.players[1:]
Exemplo n.º 15
0
class GameManager:
    """
    Allow to play bots between each other
    To have a metrics how new version plays against old versions
    """

    replay_name = ""

    tiles = None
    dead_wall = None
    clients = None
    dora_indicators = None
    players_with_open_hands = None
    discards = None
    replay = None

    dealer = None
    current_client_seat = None
    round_number = 0
    honba_sticks = 0
    riichi_sticks = 0

    _unique_dealers = 0
    _need_to_check_same_winds = None

    def __init__(self, clients, replays_directory, replay_name):
        self.tiles = []
        self.dead_wall = []
        self.dora_indicators = []
        self.discards = []
        self.clients = clients

        self.agari = Agari()
        self.finished_hand = HandCalculator()
        self.replays_directory = replays_directory
        self.replay_name = replay_name

    @staticmethod
    def generate_replay_name():
        return f"{datetime.datetime.now().strftime('%Y-%m-%d_%H_%M_%S')}_{randint(0, 99999):03}.txt"

    def init_game(self):
        """
        Beginning of the game.
        Clients random placement and dealer selection.
        """

        logger.info("Replay name: {}".format(self.replay_name))
        self.replay = TenhouReplay(self.replay_name, self.clients,
                                   self.replays_directory)

        seed(shuffle_seed())
        self.clients = self._randomly_shuffle_array(self.clients)
        for i in range(0, len(self.clients)):
            self.clients[i].seat = i

        # oya should be always first player
        # to have compatibility with tenhou format
        self.set_dealer(0)

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

        self._unique_dealers = 0
        self.round_number = 0

    def play_game(self):
        logger.info("The start of the game")

        is_game_end = False
        self.init_game()
        self.replay.init_game(shuffle_seed())

        while not is_game_end:
            self.init_round()

            results = self.play_round()

            dealer_won = False
            was_retake = False
            for result in results:
                # we want to increase honba in that case and don't move dealer seat
                if result["is_abortive_retake"]:
                    dealer_won = True

                if not result["winner"]:
                    was_retake = True
                    continue

                if result["winner"].player.is_dealer:
                    dealer_won = True

            old_dealer = self.dealer
            # if dealer won we need to increment honba sticks
            if dealer_won:
                self.honba_sticks += 1
            # otherwise let's move dealer seat
            else:
                # retake and dealer is noten
                if was_retake:
                    self.honba_sticks += 1
                else:
                    self.honba_sticks = 0

                new_dealer = self._move_position(self.dealer)
                self.set_dealer(new_dealer)

            is_game_end = self._check_the_end_of_game(old_dealer)

            # important increment, we are building wall seed based on the round number
            self.round_number += 1

        winner = self.recalculate_players_position()
        # winner takes riichi sticks
        winner.player.scores += self.riichi_sticks * 1000
        self.replay.end_game()

        logger.info("Final Scores: {0}".format(
            self.players_sorted_by_scores()))

        total_scores = sum([x.player.scores for x in self.clients])
        assert total_scores == 100000, total_scores

    def init_round(self):
        """
        Generate players hands, dead wall and dora indicators
        """
        self._need_to_check_same_winds = True

        self.players_with_open_hands = []
        self.dora_indicators = []

        self.tiles = self._generate_wall()

        for client in self.clients:
            client.erase_state()

        self.dead_wall = self._cut_tiles(14)
        self.add_new_dora_indicator()

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

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

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

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

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

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

        logger.info("Seed: {}".format(shuffle_seed()))
        logger.info("Dealer: {}, {}".format(
            self.dealer, self.clients[self.dealer].player.name))
        logger.info("Wind: {}. Riichi sticks: {}. Honba sticks: {}".format(
            self._unique_dealers, self.riichi_sticks, self.honba_sticks))
        logger.info("Round number: {}".format(self.round_number))
        logger.info("Players: {0}".format(self.players_sorted_by_scores()))

        self.replay.init_round(self.dealer, self._unique_dealers,
                               self.honba_sticks, self.riichi_sticks,
                               self.dora_indicators[0])

    def play_round(self) -> []:
        continue_to_play = True

        number_of_kan_sets_per_player = {0: 0, 1: 0, 2: 0, 3: 0}
        while continue_to_play:
            current_client = self._get_current_client()

            in_tempai = current_client.player.in_tempai

            drawn_tile = self._cut_tiles(1)[0]

            drawn_tile_34 = drawn_tile // 4
            current_client.table.count_of_remaining_tiles -= 1
            self.replay.draw(current_client.seat, drawn_tile)

            current_client.player.draw_tile(drawn_tile)
            tiles = current_client.player.tiles

            if (self.player_can_call_kyuushu_kyuuhai(current_client.player)
                    and current_client.player.should_call_kyuushu_kyuuhai()):
                return [self.abortive_retake(AbortiveDraw.NINE_DIFFERENT)]

            # win by tsumo after tile draw
            is_win = self.agari.is_agari(TilesConverter.to_34_array(tiles),
                                         current_client.player.meld_34_tiles)
            if is_win:
                tiles.remove(drawn_tile)
                can_win = True

                # with open hand it can be situation when we in the tempai
                # but our hand doesn't contain any yaku
                # in that case we can't call ron
                if not current_client.player.in_riichi:
                    result = current_client.player.ai.estimate_hand_value_or_get_from_cache(
                        drawn_tile_34,
                        is_tsumo=True,
                        is_rinshan=current_client.is_rinshan)
                    can_win = result.error is None

                if can_win:
                    result = self.process_the_end_of_the_round(
                        tiles=tiles,
                        win_tile=drawn_tile,
                        winner=current_client,
                        loser=None,
                        is_tsumo=True)
                    return [result]
                else:
                    # we can't win
                    # so let's add tile back to hand
                    # and discard it later
                    tiles.append(drawn_tile)

            # checks if we can call closed kan or shouminkan
            current_client_tiles_34 = TilesConverter.to_34_array(
                current_client.player.tiles)
            if current_client_tiles_34[drawn_tile_34] == 4 and len(
                    self.tiles) > 1:
                kan_type = current_client.player.should_call_kan(
                    drawn_tile,
                    open_kan=False,
                    from_riichi=current_client.player.in_riichi)
                if kan_type:
                    tiles = [
                        (drawn_tile_34 * 4),
                        (drawn_tile_34 * 4) + 1,
                        (drawn_tile_34 * 4) + 2,
                        (drawn_tile_34 * 4) + 3,
                    ]
                    opened = False
                    if kan_type == MeldPrint.SHOUMINKAN:
                        opened = True
                    meld = MeldPrint(
                        kan_type,
                        tiles,
                        opened=opened,
                        called_tile=drawn_tile,
                        who=current_client.seat,
                        from_who=current_client.seat,
                    )
                    logger.info("Called meld: {} by {}".format(
                        meld, current_client.player.name))

                    self.replay.open_meld(meld)

                    if opened:
                        for client in self.clients:
                            client.is_ippatsu = False

                        result = self.check_clients_possible_ron(
                            current_client,
                            drawn_tile,
                            is_tsumogiri=False,
                            is_chankan=True)

                        # the end of the round
                        if result:
                            return result

                    number_of_kan_sets_per_player[current_client.seat] += 1
                    if (sum(number_of_kan_sets_per_player.values()) == 4 and
                            len(number_of_kan_sets_per_player.values()) != 1):
                        return [self.abortive_retake(AbortiveDraw.FOUR_KANS)]

                    # we need to notify each client about called meld
                    for _client in self.clients:
                        _client.table.add_called_meld(
                            self._enemy_position(current_client.seat,
                                                 _client.seat), meld)

                    self.add_new_dora_indicator()

                    current_client.is_rinshan = True

                    # after that we will return to the current client next draw
                    continue

            # we had to clear state after tile draw
            current_client.is_ippatsu = False
            current_client.is_rinshan = False

            # if not in riichi, let's decide what tile to discard
            if not current_client.player.in_riichi:
                tile = current_client.player.discard_tile()
                in_tempai = current_client.player.in_tempai
            else:
                tile = current_client.player.discard_tile(drawn_tile,
                                                          force_tsumogiri=True)

            who_called_riichi_seat = None
            if in_tempai and not current_client.player.is_open_hand and current_client.player.can_call_riichi(
            ):
                who_called_riichi_seat = current_client.seat
                for client in self.clients:
                    client.table.add_called_riichi_step_one(
                        self._enemy_position(who_called_riichi_seat,
                                             client.seat))
                self.replay.riichi(current_client.seat, 1)

            self.replay.discard(current_client.seat, tile)
            is_tsumogiri = drawn_tile == tile
            result = self.check_clients_possible_ron(current_client, tile,
                                                     is_tsumogiri)
            # the end of the round
            if result:
                # check_clients_possible_ron already returns array
                return result

            # if there is no challenger to ron, let's check can we call riichi with tile discard or not
            if who_called_riichi_seat is not None:
                self.call_riichi(current_client)
                for client in self.clients:
                    client.table.add_called_riichi_step_two(
                        self._enemy_position(who_called_riichi_seat,
                                             client.seat))
                self.replay.riichi(current_client.seat, 2)

                count_of_riichi_players = 0
                for client in self.clients:
                    if client.player.in_riichi:
                        count_of_riichi_players += 1

                if count_of_riichi_players == 4:
                    return [self.abortive_retake(AbortiveDraw.FOUR_RIICHI)]

            # abortive retake
            result = self._check_same_winds()
            if result:
                return [result]

            # let's check other players hand to possibility open sets
            possible_melds = []

            tile_34 = tile // 4
            for other_client in self.clients:
                # there is no need to check the current client
                # or check client in riichi
                if other_client == current_client or other_client.player.in_riichi:
                    continue

                # was a tile discarded by the left player or not
                if other_client.seat == 0:
                    is_kamicha_discard = current_client.seat == 3
                else:
                    is_kamicha_discard = other_client.seat - current_client.seat == 1

                other_client.table.revealed_tiles[tile_34] -= 1

                # opened kan
                other_client_closed_hand_34 = TilesConverter.to_34_array(
                    other_client.player.closed_hand)
                if (other_client_closed_hand_34[tile_34] == 3
                        and len(self.tiles) > 1
                        and other_client.player.should_call_kan(
                            tile, open_kan=True)):
                    tiles = [
                        (tile_34 * 4),
                        (tile_34 * 4) + 1,
                        (tile_34 * 4) + 2,
                        (tile_34 * 4) + 3,
                    ]
                    meld = MeldPrint(
                        MeldPrint.KAN,
                        tiles,
                        opened=True,
                        called_tile=tile,
                        who=other_client.seat,
                        from_who=current_client.seat,
                    )
                    logger.info("Called meld: {} by {}".format(
                        meld, other_client.player.name))

                    self.replay.open_meld(meld)

                    # we changed current client seat
                    self.players_with_open_hands.append(other_client.seat)

                    number_of_kan_sets_per_player[other_client.seat] += 1
                    if (sum(number_of_kan_sets_per_player.values()) == 4 and
                            len(number_of_kan_sets_per_player.values()) != 1):
                        return [self.abortive_retake(AbortiveDraw.FOUR_KANS)]

                    # we need to notify each client about called meld
                    for _client in self.clients:
                        _client.table.add_called_meld(
                            self._enemy_position(other_client.seat,
                                                 _client.seat), meld)

                    self.add_new_dora_indicator()

                    # move to draw tile action
                    other_client.is_rinshan = True
                    self.current_client_seat = self._move_position(
                        other_client.seat, shift=-1)
                    continue

                meld, discard_option = other_client.player.try_to_call_meld(
                    tile, is_kamicha_discard)

                other_client.table.revealed_tiles[tile_34] += 1

                if meld:
                    meld.from_who = current_client.seat
                    meld.who = other_client.seat
                    meld.called_tile = tile
                    possible_melds.append({
                        "meld": meld,
                        "discard_option": discard_option
                    })

            if possible_melds:
                # pon is more important than chi
                possible_melds = sorted(
                    possible_melds, key=lambda x: x["meld"].type == Meld.PON)
                meld = possible_melds[0]["meld"]
                discard_option = possible_melds[0]["discard_option"]

                # clear ippatsu after called meld
                for client_item in self.clients:
                    client_item.is_ippatsu = False

                # we changed current client with called open set
                self.current_client_seat = meld.who
                current_client = self._get_current_client()
                self.players_with_open_hands.append(self.current_client_seat)

                logger.info("Called meld: {} by {}".format(
                    meld, current_client.player.name))

                # we need to notify each client about called meld
                for _client in self.clients:
                    _client.table.add_called_meld(
                        self._enemy_position(current_client.seat,
                                             _client.seat), meld)

                self.replay.open_meld(meld)
                current_client.player.tiles.append(tile)
                discarded_tile = current_client.player.discard_tile(
                    discard_option)

                self.replay.discard(current_client.seat, discarded_tile)

                # the end of the round
                result = self.check_clients_possible_ron(
                    current_client, discarded_tile, False)
                if result:
                    # check_clients_possible_ron already returns array
                    return result

            self.current_client_seat = self._move_position(
                self.current_client_seat)

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

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

    def check_clients_possible_ron(self,
                                   current_client,
                                   tile,
                                   is_tsumogiri,
                                   is_chankan=False) -> []:
        """
        After tile discard let's check all other players can they win or not
        at this tile
        """
        possible_win_client = []
        for other_client in self.clients:
            # there is no need to check the current client
            if other_client == current_client:
                continue

            # let's store other players discards
            if not is_chankan:
                other_client.table.add_discarded_tile(
                    self._enemy_position(current_client.seat,
                                         other_client.seat), tile,
                    is_tsumogiri)

            if self.can_call_ron(
                    other_client, tile,
                    self._enemy_position(current_client.seat,
                                         other_client.seat), is_chankan):
                possible_win_client.append(other_client)

        if len(possible_win_client) == 3:
            return [self.abortive_retake(AbortiveDraw.TRIPLE_RON)]

        # check multiple ron
        results = []
        for client in possible_win_client:
            result = self.process_the_end_of_the_round(
                tiles=client.player.tiles,
                win_tile=tile,
                winner=client,
                loser=current_client,
                is_tsumo=False,
                is_chankan=is_chankan,
            )
            results.append(result)

        return results

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

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

        # return winner of the game
        return sorted([x for x in self.clients],
                      key=lambda x: x.player.position)[0]

    def can_call_ron(self, client, win_tile, shifted_enemy_seat, is_chankan):
        if not client.player.in_tempai:
            return False

        tiles = client.player.tiles
        is_agari = self.agari.is_agari(
            TilesConverter.to_34_array(tiles + [win_tile]),
            client.player.meld_34_tiles)
        if not is_agari:
            return False

        # check for furiten
        for item in client.player.discards:
            discarded_tile = item.value // 4
            if discarded_tile in client.player.ai.waiting:
                return False

        # it can be situation when we are in agari state
        # but our hand doesn't contain any yaku
        # in that case we can't call ron
        if not client.player.in_riichi:
            result = client.player.ai.estimate_hand_value_or_get_from_cache(
                win_tile // 4,
                is_tsumo=False,
                is_chankan=is_chankan,
            )
            if result.error:
                return False

        # bot decided to not call ron
        if not client.player.should_call_win(win_tile, False,
                                             shifted_enemy_seat, is_chankan):
            return False

        return True

    def call_riichi(self, client):
        client.player.in_riichi = True
        # -1000 we will deduct in the bot logic
        self.riichi_sticks += 1

        if len(client.player.discards
               ) == 1 and not self.players_with_open_hands:
            client.is_daburi = True

        # we will set it to False after next draw
        # or called meld
        client.is_ippatsu = True

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

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

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

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

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

        if winner:
            return self.agari_result(winner, loser, is_tsumo, tiles, win_tile,
                                     is_chankan)
        else:
            return self.retake()

    def agari_result(self, winner, loser, is_tsumo, tiles, win_tile,
                     is_chankan):
        logger.info(
            "{}: {} + {}".format(
                is_tsumo and "Tsumo" or "Ron",
                TilesConverter.to_one_line_string(tiles, print_aka_dora=True),
                TilesConverter.to_one_line_string([win_tile],
                                                  print_aka_dora=True),
            ), )

        ura_dora = []
        number_of_dora_indicators = len(self.dora_indicators)
        # add one more dora for riichi win
        if winner.player.in_riichi:
            # 9 10 11 12 indices
            for x in range(number_of_dora_indicators):
                next_indicator_index = 9 + x
                ura_dora.append(self.dead_wall[next_indicator_index])

        is_tenhou = False
        # tenhou.net doesn't have renhou
        is_renhou = False
        is_chiihou = False

        if not self.players_with_open_hands and len(
                winner.player.discards) == 0 and is_tsumo:
            if winner.player.is_dealer:
                is_tenhou = True
            else:
                is_chiihou = True

        is_haitei = False
        is_houtei = False
        if not self.tiles:
            if is_tsumo:
                is_haitei = True
            else:
                is_houtei = True

        config = HandConfig(
            is_riichi=winner.player.in_riichi,
            player_wind=winner.player.player_wind,
            round_wind=winner.player.table.round_wind_tile,
            is_tsumo=is_tsumo,
            is_tenhou=is_tenhou,
            is_renhou=is_renhou,
            is_chiihou=is_chiihou,
            is_daburu_riichi=winner.is_daburi,
            is_ippatsu=winner.is_ippatsu,
            is_haitei=is_haitei,
            is_houtei=is_houtei,
            is_rinshan=winner.is_rinshan,
            is_chankan=is_chankan,
            options=OptionalRules(
                has_aka_dora=settings.FIVE_REDS,
                has_open_tanyao=settings.OPEN_TANYAO,
                has_double_yakuman=False,
            ),
        )

        hand_value = self.finished_hand.estimate_hand_value(
            tiles=tiles + [win_tile],
            win_tile=win_tile,
            melds=winner.player.melds,
            dora_indicators=self.dora_indicators + ura_dora,
            config=config,
        )

        if hand_value.error:
            logger.error("Can't estimate a hand: {}. Error: {}".format(
                TilesConverter.to_one_line_string(tiles + [win_tile],
                                                  print_aka_dora=True),
                hand_value.error))
            raise ValueError("Not correct hand")

        logger.info("Dora indicators: {}".format(
            TilesConverter.to_one_line_string(self.dora_indicators,
                                              print_aka_dora=True)))
        logger.info("Hand yaku: {}".format(", ".join(
            str(x) for x in hand_value.yaku)))

        if loser is not None:
            loser_seat = loser.seat
        else:
            # tsumo
            loser_seat = winner.seat

        self.replay.win(
            winner.seat,
            loser_seat,
            win_tile,
            self.honba_sticks,
            self.riichi_sticks,
            hand_value.han,
            hand_value.fu,
            hand_value.cost,
            hand_value.yaku,
            self.dora_indicators,
            ura_dora,
        )

        riichi_bonus = self.riichi_sticks * 1000
        self.riichi_sticks = 0
        honba_bonus = self.honba_sticks * 300

        # win by ron
        if loser:
            scores_to_pay = hand_value.cost["main"] + honba_bonus
            win_amount = scores_to_pay + riichi_bonus
            winner.player.scores += win_amount
            loser.player.scores -= scores_to_pay

            logger.info("Win:  {0} +{1:,d} +{2:,d}".format(
                winner.player.name, scores_to_pay, riichi_bonus))
            logger.info("Lose: {0} -{1:,d}".format(loser.player.name,
                                                   scores_to_pay))
        # win by tsumo
        else:
            calculated_cost = hand_value.cost[
                "main"] + hand_value.cost["additional"] * 2
            win_amount = calculated_cost + riichi_bonus + honba_bonus
            winner.player.scores += win_amount

            logger.info("Win:  {0} +{1:,d} +{2:,d}".format(
                winner.player.name, calculated_cost,
                riichi_bonus + honba_bonus))

            for client in self.clients:
                if client != winner:
                    if client.player.is_dealer:
                        scores_to_pay = hand_value.cost["main"]
                    else:
                        scores_to_pay = hand_value.cost["additional"]
                    scores_to_pay += honba_bonus / 3

                    client.player.scores -= scores_to_pay
                    logger.info("Lose: {0} -{1:,d}".format(
                        client.player.name, int(scores_to_pay)))

        return {
            "winner": winner,
            "loser": loser,
            "is_tsumo": is_tsumo,
            "is_abortive_retake": False
        }

    def retake(self):
        logger.info("Retake")
        tempai_users = []
        for client in self.clients:
            if client.player.in_tempai:
                tempai_users.append(client.seat)

        tempai_users_count = len(tempai_users)
        if tempai_users_count == 0 or tempai_users_count == 4:
            self.honba_sticks += 1
        else:
            # 1 tempai user  will get 3000
            # 2 tempai users will get 1500 each
            # 3 tempai users will get 1000 each
            scores_to_pay = 3000 / tempai_users_count
            for client in self.clients:
                if client.player.in_tempai:
                    client.player.scores += scores_to_pay
                    logger.info("{0} +{1:,d}".format(client.player.name,
                                                     int(scores_to_pay)))

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

        self.replay.retake(tempai_users, self.honba_sticks, self.riichi_sticks)
        return {
            "winner": None,
            "loser": None,
            "is_tsumo": False,
            "is_abortive_retake": False
        }

    def abortive_retake(self, reason):
        logger.info("Abortive retake. Reason: {}".format(reason))
        self.replay.abortive_retake(reason, self.honba_sticks,
                                    self.riichi_sticks)
        return {
            "winner": None,
            "loser": None,
            "is_tsumo": False,
            "is_abortive_retake": True
        }

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

    def _check_same_winds(self):
        if not self._need_to_check_same_winds:
            return None

        # with called melds this abortive retake is not possible
        if self.players_with_open_hands:
            self._need_to_check_same_winds = False
            return None

        # it is possible only for the first 4 discards
        if len(self.discards) > 4:
            self._need_to_check_same_winds = False
            return None

        # it is too early
        if len(self.discards) != 4:
            return None

        tiles = [x // 4 for x in self.discards]
        unique_tiles = list(set(tiles))

        # first 4 discards wasn't same tiles
        if len(unique_tiles) != 1:
            self._need_to_check_same_winds = False
            return None

        tile = unique_tiles[1]
        if tile in WINDS:
            return self.abortive_retake(AbortiveDraw.SAME_FIRST_WIND)
        else:
            self._need_to_check_same_winds = False
            return None

    def _check_the_end_of_game(self, dealer_seat):
        dealer_has_higher_scores = True
        has_player_with_30000_plus_scores = False
        dealer = [x for x in self.clients if x.seat == dealer_seat][0]
        for client in self.clients:
            # if someone has negative scores at the end of round, we need to end the game
            if client.player.scores < 0:
                logger.info("Game end: negative scores")
                return True

            if client.player.scores >= 30000:
                has_player_with_30000_plus_scores = True

            if client.seat == dealer.seat:
                continue

            if client.player.scores > dealer.player.scores:
                dealer_has_higher_scores = False

        # orasu ended
        if self._unique_dealers == 8 and dealer_has_higher_scores:
            logger.info(
                "Game end: dealer has higher scores at the end of south wind")
            return True

        # we have played all 8 winds (starting from wind 0)
        # and there is player with 30000+ scores
        if self._unique_dealers > 7 and has_player_with_30000_plus_scores:
            logger.info("Game end: 30000+ scores and the end of south wind")
            return True

        # west round was finished, we don't want to play north round
        if self._unique_dealers > 11:
            logger.info("Game end: the end of west wind")
            return True

        return False

    def _get_current_client(self) -> LocalClient:
        return self.clients[self.current_client_seat]

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

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

    def _move_position(self, current_position, shift=1):
        """
        Loop 0 -> 1 -> 2 -> 3 -> 0
        """
        current_position += shift
        if current_position > 3:
            current_position = 0
        if current_position < 0:
            current_position = 3
        return current_position

    def _enemy_position(self, who, from_who):
        positions = [0, 1, 2, 3]
        return positions[who - from_who]

    def _generate_wall(self):
        # round of played numbers here to be sure that each wall will be unique
        wall_seed = shuffle_seed() + self.round_number

        # init seed for random generator
        seed(wall_seed)

        wall = [i for i in range(0, 136)]
        # let's shuffle wall two times just in case
        wall = self._randomly_shuffle_array(wall)
        wall = self._randomly_shuffle_array(wall)

        return wall

    def _randomly_shuffle_array(self, array):
        rand_seeds = [randint(0, len(array) - 1) for _ in range(0, len(array))]
        # for better wall shuffling we had to do it manually
        # shuffle() didn't make wall to be really random
        for x in range(0, len(array)):
            src = x
            dst = rand_seeds[x]

            swap = array[x]
            array[src] = array[dst]
            array[dst] = swap
        return array

    def add_new_dora_indicator(self):
        number_of_dora_indicators = len(self.dora_indicators)
        # 2 3 4 5 indices
        next_indicator_index = 2 + number_of_dora_indicators
        self.dora_indicators.append(self.dead_wall[next_indicator_index])

        if number_of_dora_indicators > 0:
            self.replay.add_new_dora(self.dora_indicators[-1])

        for _client in self.clients:
            _client.table.add_dora_indicator(self.dora_indicators[-1])

    def player_can_call_kyuushu_kyuuhai(self, player):
        if len(player.discards) > 0 or len(player.melds) > 0:
            return False
        tiles_34 = [x // 4 for x in player.tiles]
        terminals_and_honors = [
            x for x in tiles_34 if is_honor(x) or is_terminal(x)
        ]
        return len(list(set(terminals_and_honors))) >= 9
Exemplo n.º 16
0
class LogParser:
    data_to_save = None

    def __init__(self):
        self.shanten = Shanten()
        self.agari = Agari()
        self.finished_hand = HandCalculator()

        self.data_to_save = []

        self.csv_exporter = CSVExporter()

    def on_player_draw(self, player, table):
        pass

    def on_player_discard(self, player, table, discarded_tile):
        pass

    def on_player_tenpai(self, player, table):
        pass

    def get_game_rounds(self, log_content, log_id):
        """
        XML parser was really slow here,
        so I built simple parser to separate log content on tags (grouped by rounds)
        """
        tag_start = 0
        rounds = []
        tag = None

        current_tags = []

        for x in range(0, len(log_content)):
            if log_content[x] == ">":
                tag = log_content[tag_start:x + 1]
                tag_start = x + 1

            # not useful tags
            skip_tags = ["SHUFFLE", "TAIKYOKU", "mjloggm", "GO", "UN"]
            if tag and any([x in tag for x in skip_tags]):
                tag = None

            # new hand was started
            if self.is_init_tag(tag) and current_tags:
                rounds.append(current_tags)
                current_tags = []

            # the end of the game
            if tag and "owari" in tag:
                rounds.append(current_tags)

            if tag:
                if self.is_init_tag(tag):
                    # we dont need seed information
                    # it appears in old logs format
                    find = re.compile(r'shuffle="[^"]*"')
                    tag = find.sub("", tag)

                    current_tags.append('<LOG_ID id="{}" />'.format(log_id))

                # add processed tag to the hand
                current_tags.append(tag)
                tag = None

        return rounds

    def parse_game_rounds(self, game):
        self.data_to_save = []
        step = 0
        for round_item in game:
            table = Table()

            log_id = None
            who_called_meld_on_this_step = None

            try:
                for tag in round_item:
                    if self.is_log_id(tag):
                        log_id = self.get_attribute_content(tag, "id")
                        table.log_id = log_id

                    if self.is_init_tag(tag):
                        seed = [
                            int(x) for x in self.get_attribute_content(
                                tag, "seed").split(",")
                        ]
                        current_hand = seed[0]
                        dora_indicator = seed[5]
                        dealer_seat = int(
                            self.get_attribute_content(tag, "oya"))
                        scores = [
                            int(x) for x in self.get_attribute_content(
                                tag, "ten").split(",")
                        ]

                        table.init(dealer_seat, current_hand, dora_indicator,
                                   step, scores)

                        table.get_player(0).init_hand(
                            self.get_attribute_content(tag, "hai0"))
                        table.get_player(1).init_hand(
                            self.get_attribute_content(tag, "hai1"))
                        table.get_player(2).init_hand(
                            self.get_attribute_content(tag, "hai2"))
                        table.get_player(3).init_hand(
                            self.get_attribute_content(tag, "hai3"))

                        step += 1

                    if self.is_draw(tag):
                        tile = self.parse_tile(tag)
                        player_seat = self.get_player_seat(tag)
                        player = table.get_player(player_seat)

                        player.draw_tile(tile)

                        self.on_player_draw(player, table)

                    if self.is_discard(tag):
                        tile = self.parse_tile(tag)
                        player_seat = self.get_player_seat(tag)
                        player = table.get_player(player_seat)

                        is_tsumogiri = tile == player.last_drawn_tile
                        after_meld = player_seat == who_called_meld_on_this_step

                        discard = Discard(tile, is_tsumogiri, after_meld,
                                          False)
                        player.discard_tile(discard)

                        tenpai_after_discard = False
                        tiles_34 = TilesConverter.to_34_array(player.tiles)
                        melds_34 = player.melds_34
                        if self.shanten.calculate_shanten(tiles_34,
                                                          melds_34) == 0:
                            tenpai_after_discard = True

                            self.on_player_tenpai(player, table)
                        else:
                            player.in_tempai = False

                        player.discards[
                            -1].tenpai_after_discard = tenpai_after_discard
                        who_called_meld_on_this_step = None
                        self.on_player_discard(player, table, tile)

                    if self.is_meld_set(tag):
                        meld = self.parse_meld(tag)
                        player = table.get_player(meld.who)

                        # when we called chankan we need to remove pon set from hand
                        if meld.type == ParserMeld.CHANKAN:
                            player.tiles.remove(meld.called_tile)
                            pon_set = [
                                x for x in player.melds
                                if x.tiles[0] == meld.tiles[0]
                            ][0]
                            player.melds.remove(pon_set)

                        player.add_meld(meld)

                        # if it was not kan/chankan let's add it to the hand
                        if meld.type != ParserMeld.CHANKAN and meld.type != ParserMeld.KAN:
                            player.tiles.append(meld.called_tile)

                        # indication that tile was taken from discard
                        if meld.opened:
                            for meld_player in table.players:
                                if meld_player.discards and meld_player.discards[
                                        -1].tile == meld.called_tile:
                                    meld_player.discards[
                                        -1].was_given_for_meld = True

                        # for closed kan we had to remove tile from hand
                        if (meld.type == ParserMeld.KAN and not meld.opened
                                and meld.called_tile in player.tiles):
                            player.tiles.remove(meld.called_tile)

                        who_called_meld_on_this_step = meld.who

                    if self.is_riichi(tag):
                        riichi_step = int(
                            self.get_attribute_content(tag, "step"))
                        who = int(self.get_attribute_content(tag, "who"))
                        player = table.get_player(who)

                        if riichi_step == 1:
                            player.in_riichi = True

                        if riichi_step == 2:
                            player.discards[-1].after_riichi = True

                    if self.is_new_dora(tag):
                        dora = int(self.get_attribute_content(tag, "hai"))
                        table.add_dora(dora)

            except Exception as e:
                logger.error("Failed to process log: {}".format(log_id))
                logger.error(e, exc_info=True)

        return self.data_to_save

    def get_player_waiting(self, player):
        tiles = player.closed_hand
        if len(tiles) == 1:
            return [tiles[0] // 4]

        tiles_34 = TilesConverter.to_34_array(tiles)

        waiting = []
        for j in range(0, 34):
            # we already have 4 tiles in hand
            # and we can't wait on 5th
            if tiles_34[j] == 4:
                continue

            tiles_34[j] += 1
            if self.agari.is_agari(tiles_34):
                waiting.append(j)
            tiles_34[j] -= 1

        return waiting

    def calculate_waiting_costs(self, player, player_waiting):
        waiting = []
        for tile in player_waiting:
            config = HandConfig(
                is_riichi=player.discards[-1].after_riichi,
                player_wind=player.player_wind,
                round_wind=player.table.round_wind,
                options=OptionalRules(has_aka_dora=True, has_open_tanyao=True),
            )

            win_tile = tile * 4
            # we don't need to think, that our waiting is aka dora
            if win_tile in AKA_DORA_LIST:
                win_tile += 1

            tiles = player.tiles + [win_tile]

            result = self.finished_hand.estimate_hand_value(
                tiles, win_tile, player.melds, player.table.dora_indicators,
                config)

            if result.error:
                waiting.append({
                    "tile": win_tile,
                    "han": None,
                    "fu": None,
                    "cost": 0,
                    "yaku": []
                })
            else:
                waiting.append({
                    "tile":
                    win_tile,
                    "han":
                    result.han,
                    "fu":
                    result.fu,
                    "cost":
                    result.cost["main"],
                    "yaku": [{
                        "id": x.yaku_id,
                        "name": x.name
                    } for x in result.yaku],
                })

        return waiting

    def get_attribute_content(self, tag, attribute_name):
        result = re.findall(r'{}="([^"]*)"'.format(attribute_name), tag)
        return result and result[0] or None

    def is_discard(self, tag):
        skip_tags = ["<GO", "<FURITEN", "<DORA"]
        if any([x in tag for x in skip_tags]):
            return False

        match_discard = re.match(r"^<[defgDEFG]+\d*", tag)
        if match_discard:
            return True

        return False

    def is_draw(self, tag):
        match_discard = re.match(r"^<[tuvwTUVW]+\d*", tag)
        if match_discard:
            return True

        return False

    def parse_tile(self, message):
        result = re.match(r"^<[defgtuvwDEFGTUVW]+\d*", message).group()
        return int(result[2:])

    def get_player_seat(self, tag):
        player_sign = tag.lower()[1]
        if player_sign == "d" or player_sign == "t":
            player_seat = 0
        elif player_sign == "e" or player_sign == "u":
            player_seat = 1
        elif player_sign == "f" or player_sign == "v":
            player_seat = 2
        else:
            player_seat = 3

        return player_seat

    def parse_meld(self, tag):
        who = int(self.get_attribute_content(tag, "who"))
        data = int(self.get_attribute_content(tag, "m"))

        meld = ParserMeld()
        meld.who = who
        meld.from_who = ((data & 0x3) + meld.who) % 4

        if data & 0x4:
            self.parse_chi(data, meld)
        elif data & 0x18:
            self.parse_pon(data, meld)
        elif data & 0x20:
            # nuki
            pass
        else:
            self.parse_kan(data, meld)

        return meld

    def parse_chi(self, data, meld):
        meld.type = ParserMeld.CHI
        t0, t1, t2 = (data >> 3) & 0x3, (data >> 5) & 0x3, (data >> 7) & 0x3
        base_and_called = data >> 10
        base = base_and_called // 3
        called = base_and_called % 3
        base = (base // 7) * 9 + base % 7
        meld.tiles = [
            t0 + 4 * (base + 0), t1 + 4 * (base + 1), t2 + 4 * (base + 2)
        ]
        meld.called_tile = meld.tiles[called]

    def parse_pon(self, data, meld):
        t4 = (data >> 5) & 0x3
        t0, t1, t2 = ((1, 2, 3), (0, 2, 3), (0, 1, 3), (0, 1, 2))[t4]
        base_and_called = data >> 9
        base = base_and_called // 3
        called = base_and_called % 3
        if data & 0x8:
            meld.type = ParserMeld.PON
            meld.tiles = [t0 + 4 * base, t1 + 4 * base, t2 + 4 * base]
            meld.called_tile = meld.tiles[called]
        else:
            meld.type = ParserMeld.CHANKAN
            meld.tiles = [
                t0 + 4 * base, t1 + 4 * base, t2 + 4 * base, t4 + 4 * base
            ]
            meld.called_tile = meld.tiles[3]

    def parse_kan(self, data, meld):
        base_and_called = data >> 8
        base = base_and_called // 4
        meld.type = ParserMeld.KAN
        meld.tiles = [4 * base, 1 + 4 * base, 2 + 4 * base, 3 + 4 * base]
        called = base_and_called % 4
        meld.called_tile = meld.tiles[called]
        # to mark closed\opened kans
        meld.opened = meld.who != meld.from_who

    def is_init_tag(self, tag):
        return tag and "INIT" in tag

    def is_redraw_tag(self, tag):
        return tag and "RYUUKYOKU" in tag

    def is_agari_tag(self, tag):
        return tag and "AGARI" in tag

    def is_log_id(self, tag):
        return tag and "LOG_ID" in tag

    def is_meld_set(self, tag):
        return tag and "<N who=" in tag

    def is_riichi(self, tag):
        return tag and "REACH " in tag

    def is_new_dora(self, tag):
        return tag and "<DORA" in tag
Exemplo n.º 17
0
class ImplementationAI(InterfaceAI):
    version = '0.0.1'

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

    previous_shanten = 7
    in_defence = False
    waiting = None

    current_strategy = None

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

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

    def init_hand(self):
        """
        Let's decide what we will do with our hand (like open for tanyao and etc.)
        """
        self.determine_strategy()

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

    def draw_tile(self, tile):
        """
        :param tile: 136 tile format
        :return:
        """
        self.determine_strategy()

    def discard_tile(self, discard_tile):
        # we called meld and we had discard tile that we wanted to discard
        if discard_tile is not None:
            if not self.last_discard_option:
                return discard_tile

            return self.process_discard_option(self.last_discard_option,
                                               self.player.closed_hand, True)

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

        selected_tile = self.process_discard_options_and_select_tile_to_discard(
            results, shanten)

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

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

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

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

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

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

        return self.chose_tile_to_discard(results)

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

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

            tiles_34[hand_tile] -= 1

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

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

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

            tiles_34[hand_tile] += 1

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

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

        return results, shanten

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

    def try_to_call_meld(self, tile, is_kamicha_discard):
        if not self.current_strategy:
            return None, None
        if len(self.player.discards) <= 5 and tile // 4 <= 27:
            return None, None
        meld, discard_option = self.current_strategy.try_to_call_meld(
            tile, is_kamicha_discard)
        tile_to_discard = None
        if discard_option:
            self.last_discard_option = discard_option
            tile_to_discard = discard_option.tile_to_discard

        return meld, tile_to_discard

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

        # order is important, the first appropriate strtegy will be used
        strategies = []

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

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

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

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

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

        return self.current_strategy and True or False

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

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

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

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

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

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

        return selected_tile

    def process_discard_option(self,
                               discard_option,
                               closed_hand,
                               force_discard=False):
        self.waiting = discard_option.waiting
        self.player.ai.previous_shanten = discard_option.shanten
        self.player.in_tempai = self.player.ai.previous_shanten == 0

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

        last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None
        if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard:
            return self.player.last_draw
        else:
            return discard_option.find_tile_in_hand(closed_hand)

    def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False):
        """
        :param win_tile: 34 tile format
        :param tiles:
        :param call_riichi:
        :return:
        """
        win_tile *= 4

        # we don't need to think, that our waiting is aka dora
        if win_tile in AKA_DORA_LIST:
            win_tile += 1

        if not tiles:
            tiles = self.player.tiles

        tiles += [win_tile]

        config = HandConfig(is_riichi=call_riichi,
                            player_wind=self.player.player_wind,
                            round_wind=self.player.table.round_wind,
                            has_aka_dora=self.player.table.has_aka_dora,
                            has_open_tanyao=self.player.table.has_open_tanyao)

        result = self.finished_hand.estimate_hand_value(
            tiles, win_tile, self.player.melds,
            self.player.table.dora_indicators, config)
        return result

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

        if self.in_defence:
            return False

        #If we tenpai fast enough
        if len(self.player.discards) <= 8:
            return True
        if len(self.player.discards) >= 14:
            return False

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

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

        tiles_34 = TilesConverter.to_34_array(tiles)

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

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

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

        return True

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

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

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

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

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

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

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

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

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

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

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

        return None

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

    def enemy_called_riichi(self, enemy_seat):
        """
        After enemy riichi we had to check will we fold or not
        it is affect open hand decisions
        :return:
        """
        if self.defence.should_go_to_defence_mode():
            self.in_defence = True

    @property
    def enemy_players(self):
        """
        Return list of players except our bot
        """
        return self.player.table.players[1:]
Exemplo n.º 18
0
    k += 1
    w = Wall()
    hand = Hand(w)
    hand.draw(w)

cend = time.time_ns()

# Test Code
k = 0
start = time.time_ns()
while k < ops:
    k += 1
    w = Wall()
    hand = Hand(w)
    hand.draw(w)
    win = ag.is_agari(hand.tiles34)

end = time.time_ns()

print('Operation Time: {:.4f}us/op'.format((end - start) / (1e3 * ops)))
print('Control Time: {:.4f}us/op'.format((cend - cstart) / (1e3 * ops)))
print()
op_rate = (1e9 * ops) / (end - start)
ctrl_rate = (1e9 * ops) / (cend - cstart)
count_rate = 1e9 * ops / ((end - start) - (cend - cstart))
print('Net Operation Time: {:.4f}us/op'.format((end - start) / (1e3 * ops) -
                                               (cend - cstart) / (1e3 * ops)))
print('Operation Rate: ' + str(int(count_rate)) + u' \u00B1 ' +
      '{:.1f}ops/s'.format(
          np.sqrt(1e9 * (op_rate / (end - start) + ctrl_rate /
                         (cend - cstart)))))