Ejemplo n.º 1
0
    def __init__(self, player):
        super(ImplementationAI, self).__init__(player)

        self.agari = Agari()
        self.shanten_calculator = Shanten()
        self.defence = DefenceHandler(player)
        self.riichi = Riichi(player)
        self.hand_divider = HandDivider()
        self.finished_hand = HandCalculator()
        self.hand_builder = HandBuilder(player, self)

        self.erase_state()
Ejemplo n.º 2
0
    def test_chinroutou(self):
        hand = HandCalculator()

        tiles = self._string_to_136_array(man="111999", sou="11999", pin="111")
        win_tile = self._string_to_136_tile(sou="9")
        melds = [
            self._make_meld(Meld.PON, pin="111"),
        ]

        result = hand.estimate_hand_value(
            tiles,
            win_tile,
            melds=melds,
            scores_calculator_factory=Aotenjou,
            config=self._make_hand_config(is_tsumo=True,
                                          player_wind=EAST,
                                          round_wind=EAST),
        )
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 15)
        self.assertEqual(result.fu, 50)
        self.assertEqual(len(result.yaku), 2)
        self.assertEqual(result.cost["main"] + result.cost["additional"],
                         26214400)
Ejemplo n.º 3
0
    def test_shousuushii(self):
        hand = HandCalculator()

        tiles = self._string_to_136_array(sou="123", honors="11122233444")
        win_tile = self._string_to_136_tile(honors="2")
        melds = [
            self._make_meld(Meld.PON, honors="444"),
        ]

        result = hand.estimate_hand_value(
            tiles,
            win_tile,
            melds=melds,
            scores_calculator_factory=Aotenjou,
            config=self._make_hand_config(is_tsumo=True,
                                          player_wind=EAST,
                                          round_wind=EAST),
        )
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 18)
        self.assertEqual(result.fu, 50)
        self.assertEqual(len(result.yaku), 5)
        self.assertEqual(result.cost["main"] + result.cost["additional"],
                         209715200)
    def test_sextuple_yakuman(self):
        hand = HandCalculator()

        # 1112223334445z 5z tenhou & tsuisou & daisushi & suuankou tanki
        tiles = self._string_to_136_array(honors="11122233344455")
        win_tile = self._string_to_136_tile(honors="5")

        config = self._make_hand_config(is_tsumo=True,
                                        is_tenhou=True,
                                        disable_double_yakuman=False)

        result = hand.estimate_hand_value(tiles, win_tile, config=config)
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 78)
        self.assertEqual(result.cost["main"], 96000)
        self.assertEqual(result.cost["additional"], 48000)

        # 5z -11-z -22-z -33-z -44-z 5z suukantsu & tsuisou & daisushi & suuankou tanki
        tiles = self._string_to_136_array(honors="111122223333444455")
        win_tile = self._string_to_136_tile(honors="5")

        config = self._make_hand_config(disable_double_yakuman=False)
        melds = [
            self._make_meld(Meld.KAN, is_open=False, honors="1111"),
            self._make_meld(Meld.KAN, is_open=False, honors="2222"),
            self._make_meld(Meld.KAN, is_open=False, honors="3333"),
            self._make_meld(Meld.KAN, is_open=False, honors="4444"),
        ]

        result = hand.estimate_hand_value(tiles,
                                          win_tile,
                                          melds=melds,
                                          config=config)
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 78)
        self.assertEqual(result.cost["main"], 192000)
Ejemplo n.º 5
0
    def test_is_suuankou(self):
        hand = HandCalculator()

        tiles = self._string_to_34_array(sou="111444", man="333", pin="44555")
        win_tile = self._string_to_136_tile(sou="4")

        self.assertTrue(
            self.config.suuankou.is_condition_met(self._hand(tiles), win_tile,
                                                  True))
        self.assertFalse(
            self.config.suuankou.is_condition_met(self._hand(tiles), win_tile,
                                                  False))

        tiles = self._string_to_136_array(sou="111444", man="333", pin="44555")
        win_tile = self._string_to_136_tile(pin="5")

        result = hand.estimate_hand_value(
            tiles, win_tile, config=self._make_hand_config(is_tsumo=True))
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 13)
        self.assertEqual(result.fu, 50)
        self.assertEqual(len(result.yaku), 1)

        result = hand.estimate_hand_value(
            tiles, win_tile, config=self._make_hand_config(is_tsumo=False))
        self.assertNotEqual(result.han, 13)

        tiles = self._string_to_136_array(sou="111444", man="333", pin="44455")
        win_tile = self._string_to_136_tile(pin="5")

        result = hand.estimate_hand_value(
            tiles, win_tile, config=self._make_hand_config(is_tsumo=True))
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 26)
        self.assertEqual(result.fu, 50)
        self.assertEqual(len(result.yaku), 1)

        tiles = self._string_to_136_array(man="33344455577799")
        win_tile = self._string_to_136_tile(man="9")

        result = hand.estimate_hand_value(
            tiles, win_tile, config=self._make_hand_config(is_tsumo=False))
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 26)
        self.assertEqual(result.fu, 50)
        self.assertEqual(len(result.yaku), 1)
Ejemplo n.º 6
0
def test_game(ai):
    pai = TEST_INIT_PAI.copy()
    random.shuffle(pai)

    calculator = HandCalculator()
    config = HandConfig(is_tsumo=True)
    config.yaku.yakuhai_place = config.yaku.east
    config.yaku.yakuhai_round = config.yaku.east

    # 1. 내 초기 패 13개 + 쯔모 1개
    hand = pai[:14].copy()
    tsumo = pai[13]

    # 1 - a.  nn_ai를 돌리는 경우에 env에 정보를 입력한다.
    err = 0
    isGameOver = False

    # 2. tsumo 하자
    for x in range(1, TEST_STEP + 1):

        # DEBUG: 정상 작동하는지 중간 패 과정 출력
        logging.debug("%02d: %s", x, TilesConverter.to_one_line_string(hand))
        # 점수 확인
        result = calculator.estimate_hand_value(hand, tsumo)
        if not result.error:
            # 화료가 되었다는 뜻이다. 친의 쯔모인 3배 점수를 반환하자
            isGameOver = True
            return result.cost['main'] * 3

        if x == TEST_STEP:
            break
        '''
        # 여기부터
        # 버린다
        discard = ai.next_move(hand, tsumo, TEST_STEP - x - 1)  # 일반적인 경우 이거 쓰기
        hand.remove(discard)
        # 쯔모한다
        tsumo = pai[14 + x]
        hand.append(tsumo)
        # 여기까지는 nn_ai 아닐때 쓰는 코드
        '''

        #여기부터
        discard_action = ai.next_move(hand, tsumo, TEST_STEP - x - 1, env, X,
                                      output_layer, nbActions, isGameOver,
                                      epsilon)  #nn_ai이면 이 코드로 변경
        discard = hand[discard_action]
        currentState = hand.copy()
        hand.remove(discard)
        tsumo = pai[14 + x]
        hand.append(tsumo)
        nextState = hand.copy()
        result = calculator.estimate_hand_value(hand, tsumo)
        if not result.error:
            isGameOver = True
            reward = result.cost['main'] * 3
        else:
            hand_34 = TilesConverter.to_34_array(hand)
            shanten = Shanten()
            result = shanten.calculate_shanten(hand_34)
            logging.debug('@: %d', result)
            if result == 0:
                reward = TEST_TEN_SCORE
            reward = TEST_NOTEN_SCORE
        memory.Reward_Handling(currentState, discard_action, reward, nextState,
                               isGameOver, env, err, epsilon,
                               epsilonMinimumValue, output_layer, batchSize,
                               nbActions, nbStates, X, Y, optimizer, cost)
        #여기까지는 nn_ai쓸때만 쓰는 코드

    # 마지막으로 텐파이인지 확인한다
    # 샹텐수가 0이면 텐파이이다
    hand_34 = TilesConverter.to_34_array(hand)
    shanten = Shanten()
    result = shanten.calculate_shanten(hand_34)
    logging.debug('@: %d', result)
    if result == 0:
        return TEST_TEN_SCORE
    return TEST_NOTEN_SCORE
Ejemplo n.º 7
0
class Phoenix:
    def __init__(self, player):
        self.player = player
        self.table = player.table

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

        self.erase_state()

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

    def collect_experience(self):

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

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

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

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

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

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

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

    def draw_tile(self):
        pass

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

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

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

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

        if not should_chi and not should_pon:
            return None, None

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

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

        return meld, discard_option

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.hand_cache_estimation[cache_key] = result
        return result

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

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

    def get_possible_meld(self, tile, is_kamicha_discard):

        closed_hand = self.player.closed_hand[:]

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

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

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

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

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

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

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

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

        return melds_chi, melds_pon

    def enemy_called_riichi(self, enemy_seat):
        """
        After enemy riichi we had to check will we fold or not
        it is affect open hand decisions
        :return:
        """
        pass
Ejemplo n.º 8
0
    def test_kokushi_musou_multiple_yakuman(self):
        hand_calculator = HandCalculator()

        # kokushi test

        tiles = TilesConverter.string_to_136_array(sou="19",
                                                   pin="19",
                                                   man="19",
                                                   honors="12345677")
        win_tile = TilesConverter.string_to_136_array(honors="1")[0]

        hand_config = HandConfig(is_tsumo=True,
                                 is_tenhou=False,
                                 is_chiihou=False)

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 1)
        self.assertTrue(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertFalse(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 13)

        hand_config = HandConfig(is_tsumo=True,
                                 is_tenhou=True,
                                 is_chiihou=False)

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 2)
        self.assertTrue(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertFalse(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertTrue(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 26)

        hand_config = HandConfig(is_tsumo=True,
                                 is_tenhou=False,
                                 is_chiihou=True)

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 2)
        self.assertTrue(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertFalse(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertTrue(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 26)

        # double kokushi test

        tiles = TilesConverter.string_to_136_array(sou="19",
                                                   pin="19",
                                                   man="19",
                                                   honors="12345677")
        win_tile = TilesConverter.string_to_136_array(honors="7")[0]

        hand_config = HandConfig(is_tsumo=True,
                                 is_tenhou=False,
                                 is_chiihou=False)

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 1)
        self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertTrue(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 26)

        hand_config = HandConfig(is_tsumo=True,
                                 is_tenhou=True,
                                 is_chiihou=False)

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 2)
        self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertTrue(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertTrue(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 39)

        hand_config = HandConfig(is_tsumo=True,
                                 is_tenhou=False,
                                 is_chiihou=True)

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 2)
        self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertTrue(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertTrue(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 39)

        hand_config = HandConfig(is_tsumo=False,
                                 is_renhou=True,
                                 options=OptionalRules(renhou_as_yakuman=True))

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 2)
        self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertTrue(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertTrue(
            hand_config.yaku.renhou_yakuman in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 39)

        hand_config = HandConfig(
            is_tsumo=False,
            is_renhou=True,
            is_riichi=True,
            is_open_riichi=True,
            options=OptionalRules(renhou_as_yakuman=True,
                                  has_sashikomi_yakuman=True),
        )

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 3)
        self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertTrue(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertTrue(
            hand_config.yaku.renhou_yakuman in hand_calculation.yaku)
        self.assertTrue(hand_config.yaku.sashikomi in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 52)
Ejemplo n.º 9
0
    def test_kokushi_musou_multiple_yakuman(self):
        hand_calculator = HandCalculator()

        # kokushi test

        tiles = TilesConverter.string_to_136_array(sou='19',
                                                   pin='19',
                                                   man='19',
                                                   honors='12345677')
        win_tile = TilesConverter.string_to_136_array(honors='1')[0]

        hand_config = HandConfig(is_tsumo=True,
                                 is_tenhou=False,
                                 is_chiihou=False)

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 1)
        self.assertTrue(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertFalse(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 13)

        hand_config = HandConfig(is_tsumo=True,
                                 is_tenhou=True,
                                 is_chiihou=False)

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 2)
        self.assertTrue(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertFalse(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertTrue(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 26)

        hand_config = HandConfig(is_tsumo=True,
                                 is_tenhou=False,
                                 is_chiihou=True)

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 2)
        self.assertTrue(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertFalse(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertTrue(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 26)

        # double kokushi test

        tiles = TilesConverter.string_to_136_array(sou='19',
                                                   pin='19',
                                                   man='19',
                                                   honors='12345677')
        win_tile = TilesConverter.string_to_136_array(honors='7')[0]

        hand_config = HandConfig(is_tsumo=True,
                                 is_tenhou=False,
                                 is_chiihou=False)

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 1)
        self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertTrue(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 26)

        hand_config = HandConfig(is_tsumo=True,
                                 is_tenhou=True,
                                 is_chiihou=False)

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 2)
        self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertTrue(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertTrue(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 39)

        hand_config = HandConfig(is_tsumo=True,
                                 is_tenhou=False,
                                 is_chiihou=True)

        hand_calculation = hand_calculator.estimate_hand_value(
            tiles, win_tile, config=hand_config)

        self.assertIsNone(hand_calculation.error)
        self.assertEqual(len(hand_calculation.yaku), 2)
        self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku)
        self.assertTrue(
            hand_config.yaku.daburu_kokushi in hand_calculation.yaku)
        self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku)
        self.assertTrue(hand_config.yaku.chiihou in hand_calculation.yaku)
        self.assertEqual(hand_calculation.han, 39)
Ejemplo n.º 10
0
from mahjong.hand_calculating.hand import HandCalculator
from mahjong.meld import Meld
from mahjong.hand_calculating.hand_config import HandConfig, OptionalRules
from mahjong.shanten import Shanten
from mahjong.tile import TilesConverter
from mahjong.hand_calculating.divider import HandDivider
from mahjong.constants import WINDS
import json
calculator = HandCalculator()


def yaku_json_create(x, opened):
    if opened:
        return {"name": x.name, "japanese": x.japanese, "han": x.han_open}
    else:
        return {"name": x.name, "japanese": x.japanese, "han": x.han_closed}


def yaku_is_yakuman(yaku):
    for x in yaku:
        if x.is_yakuman:
            return "役満"

    return "役満なし"


# useful helper
def print_hand_result(hand_result, opened=False):
    #print(hand_result.han, hand_result.fu)
    #print(hand_result.cost['main'])
    #print(hand_result.yaku)
Ejemplo n.º 11
0
class HandSolver(object):
    tiles = []
    Shanten_calculater = None
    shanten = 8
    Tiles = None
    improve_tiles_count = 0
    Hand_Calculator = None
    cnt = 0
    Hand_Config = None
    dora_indicators = []
    revealed_tiles = None
    cnt = 0

    def __init__(self, tiles, dora_indicators, revealed_tiles):
        self.tiles = tiles
        self.Shanten_calculater = Shanten()
        self.Hand_Calculator = HandCalculator()
        self.Tiles = TilesConverter()
        self.shanten = self.Shanten_calculater.calculate_shanten(
            self.Tiles.to_34_array(self.tiles))
        self.Hand_Config = HandConfig(is_riichi=True)
        self.dora_indicators = dora_indicators
        self.revealed_tiles = revealed_tiles

    def choose_tile_to_discard(self):
        if self.shanten > 1:
            return self.pure_speed_strategy()
        max_value = 0
        option_list = self.get_option_list(self.tiles)
        temp_tiles = list(self.tiles)
        option_value = [0] * 34
        for option in option_list:
            while option not in temp_tiles:
                option += 1
            temp_tiles.remove(option)
            option_value[option // 4] = self.estimate_value(
                temp_tiles, self.shanten, 1, self.shanten)
            temp_tiles.append(option)
        max_value = max(option_value)
        result = option_value.index(max_value) * 4
        while result not in self.tiles:
            result += 1
        return result

    def sort_tiles(self):
        if self.shanten == 0:
            return 'IS_TEMPAI'

    def calculate_Improve_tiles_count(self, tiles):
        cnt = 0
        for tile in range(0, 135, 4):
            now_shanten = self.Shanten_calculater.calculate_shanten(
                self.Tiles.to_34_array(tiles))
            tiles.append(tile)
            if self.Shanten_calculater.calculate_shanten(
                    self.Tiles.to_34_array(tiles)) < now_shanten:
                cnt += 4 - self.revealed_tiles[tile // 4]
            tiles.remove(tile)
        return cnt

    def get_option_list(self, tiles):  #the input are 14-tiles
        temp_tiles = list(tiles)
        option_list = []
        current_shanten = self.Shanten_calculater.calculate_shanten(
            self.Tiles.to_34_array(temp_tiles))
        for tile in tiles:
            if tile // 4 * 4 in option_list:
                continue
            temp_tiles.remove(tile)
            if self.Shanten_calculater.calculate_shanten(
                    self.Tiles.to_34_array(temp_tiles)) <= current_shanten:
                option_list.append(tile // 4 * 4)
            temp_tiles.append(tile)
        return option_list

    def pure_speed_strategy(self):
        max_improve_tiles_count = 0
        answer = 0
        option_list = self.get_option_list(self.tiles)
        temp_tiles = list(self.tiles)
        for tile in option_list:
            while tile not in temp_tiles:
                tile += 1
            temp_tiles.remove(tile)
            temp_improve_tiles_count = self.calculate_Improve_tiles_count(
                temp_tiles)
            if max_improve_tiles_count <= temp_improve_tiles_count:
                max_improve_tiles_count = temp_improve_tiles_count
                answer = tile
            temp_tiles.append(tile)
        return answer

    def solve_tempai(self):
        win_tiles = []
        temp_tiles = list(self.tiles)
        have_han = 13
        for tile in self.tiles:

            temp_tiles.remove(tile)
            if self.Shanten_calculater.calculate_shanten(
                    self.Tiles.to_34_array(temp_tiles)) <= self.shanten:
                option_list.append(tile)
            temp_tiles.append(tile)
        for options in option_list:
            temp_tiles.remove(option)
            for tile in range(0, 135):
                temp_tiles.append(tile)
                if self.Shanten_calculater.calculate_shanten(
                        self.Tiles.to_34_array(temp_tiles)) == -1:
                    have_han = min(
                        self.Hand_Calculator.estimate_hand_value(temp_tiles),
                        have_han)
                temp_tiles.remove(tile)
            temp_tiles.append(option)
        if have_han == 0:
            pass

    def get_simple_win_rate(self, win_tile, is_jinpai, remain_number):
        rate = 0
        if win_tile == 'Middle_Tile':
            if is_jinpai == 0:
                rate = 0.37
            elif is_jinpai == 1:
                rate = 0.40
            elif is_jinpai == 2:
                rate = 0.5
            rate -= (4 - remain_number) * 0.07
        elif win_tile == 'Three_Like_Tiles':
            if is_jinpai == 0:
                rate = 0.43
            else:
                rate = 0.5
            rate -= (4 - remain_number) * 0.07
        elif win_tile == 'Two_Like_Tiles':
            if is_jinpai == 0:
                rate = 0.46
            else:
                rate = 0.55
            rate -= (4 - remain_number) * 0.07
        elif win_tile == 'One_Like_Tiles':
            if is_jinpai == 0:
                rate = 0.5
            else:
                rate = 0.6
            rate -= (4 - remain_number) * 0.03
        elif win_tile == 'Honor_Tiles':
            if remain_number == 2:
                rate = 0.6
            else:
                rate = 0.55
        return rate

    def get_win_rate(self):
        pass

    def get_sort_of_tile(self, tile):
        temp_tiles = []
        temp_tiles.append(tile)
        temp_tile_string = self.Tiles.to_one_line_string(temp_tiles)
        middle_tiles = ['4m', '5m', '6m', '4s', '5s', '6s', '4p', '5p', '6p']
        three_like_tiles = ['3m', '7m', '3p', '7p', '3s', '7s']
        two_like_tiles = ['2m', '8m', '2p', '8p', '2s', '8s']
        one_like_tiles = ['1m', '9m', '1s', '9s', '1p', '9p']
        if temp_tile_string in middle_tiles:
            return 'Middle_Tile'
        elif temp_tile_string in three_like_tiles:
            return 'Three_Like_Tiles'
        elif temp_tile_string in two_like_tiles:
            return 'Two_Like_Tiles'
        elif temp_tile_string in one_like_tiles:
            return 'One_Like_Tiles'
        else:
            return 'Honor_Tiles'

    def estimate_value(self, tiles, current_shanten, depth,
                       initial_shanten):  #the input are 13 tiles
        value = 0
        temp_tiles = list(tiles)
        if current_shanten == 0:
            return self.estimate_tempai_value(tiles)
        for tile in range(0, 135, 4):
            temp_tiles.append(tile)  #search changes and turn to new tiles
            feature_shanten = self.Shanten_calculater.calculate_shanten(
                self.Tiles.to_34_array(temp_tiles))
            if feature_shanten < current_shanten:
                option_list = self.get_option_list(temp_tiles)
                flag = 0

                temp_tiles.remove(tile)
                current_improve_tiles_count = self.calculate_Improve_tiles_count(
                    temp_tiles)
                max_improve_tiles_count = current_improve_tiles_count
                temp_tiles.append(tile)
                if feature_shanten == current_shanten:  #在分析改良
                    for option in option_list:
                        while option not in temp_tiles:
                            option += 1
                        temp_tiles.remove(option)
                        max_improve_tiles_count = max(
                            max_improve_tiles_count,
                            self.calculate_Improve_tiles_count(temp_tiles))
                        temp_tiles.append(option)
                    if max_improve_tiles_count == current_improve_tiles_count:
                        temp_tiles.remove(tile)
                        continue

                max_value = 0
                for option in option_list:
                    while option not in temp_tiles:
                        option += 1
                    temp_tiles.remove(option)
                    if feature_shanten == current_shanten:
                        max_value = max(
                            self.estimate_value(temp_tiles, feature_shanten,
                                                depth + 1, initial_shanten),
                            max_value)
                    else:
                        max_value = max(
                            self.estimate_value(temp_tiles, feature_shanten,
                                                depth, initial_shanten),
                            max_value)
                    temp_tiles.append(option)
                value += max_value * (4 - self.revealed_tiles[tile // 4])
            temp_tiles.remove(tile)
        return value / 100

    def estimate_tempai_value(self, tiles):  #the input are 13 tiles
        feature_value = 0
        for tile in range(0, 135, 4):
            tiles.append(tile)
            if self.Shanten_calculater.calculate_shanten(
                    self.Tiles.to_34_array(tiles)) == -1:
                result = self.Hand_Calculator.estimate_hand_value(
                    tiles,
                    tile,
                    dora_indicators=self.dora_indicators,
                    config=self.Hand_Config)
                feature_value += result.cost['main'] * (
                    4 - self.revealed_tiles[tile // 4])
            tiles.remove(tile)
        return feature_value
Ejemplo n.º 12
0
    def test_aotenjou_hands(self):
        hand = HandCalculator()

        tiles = self._string_to_136_array(sou="119",
                                          man="19",
                                          pin="19",
                                          honors="1234567")
        win_tile = self._string_to_136_tile(sou="1")

        result = hand.estimate_hand_value(
            tiles,
            win_tile,
            scores_calculator_factory=Aotenjou,
            config=self._make_hand_config(player_wind=EAST,
                                          round_wind=EAST,
                                          disable_double_yakuman=True),
        )
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 13)
        self.assertEqual(result.fu, 40)
        self.assertEqual(len(result.yaku), 1)
        self.assertEqual(result.cost["main"], 7864400)

        tiles = self._string_to_136_array(man="234", honors="11122233344")
        win_tile = self._string_to_136_tile(man="2")
        melds = [
            self._make_meld(Meld.PON, honors="111"),
            self._make_meld(Meld.PON, honors="333"),
        ]

        result = hand.estimate_hand_value(
            tiles,
            win_tile,
            melds=melds,
            scores_calculator_factory=Aotenjou,
            config=self._make_hand_config(is_tsumo=True,
                                          player_wind=EAST,
                                          round_wind=EAST),
        )
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 17)
        self.assertEqual(result.fu, 40)
        self.assertEqual(len(result.yaku), 4)
        self.assertEqual(result.cost["main"] + result.cost["additional"],
                         83886200)

        tiles = self._string_to_136_array(honors="11122233444777")
        win_tile = self._string_to_136_tile(honors="2")
        melds = [
            self._make_meld(Meld.PON, honors="444"),
        ]

        result = hand.estimate_hand_value(
            tiles,
            win_tile,
            melds=melds,
            scores_calculator_factory=Aotenjou,
            config=self._make_hand_config(is_tsumo=True,
                                          player_wind=EAST,
                                          round_wind=EAST),
        )
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 31)
        self.assertEqual(result.fu, 50)
        self.assertEqual(len(result.yaku), 6)
        self.assertEqual(result.cost["main"] + result.cost["additional"],
                         1717986918400)

        # monster hand for fun

        tiles = self._string_to_136_array(honors="111133555566667777")
        win_tile = self._string_to_136_tile(honors="3")

        melds = [
            self._make_meld(Meld.KAN, honors="1111", is_open=False),
            self._make_meld(Meld.KAN, honors="5555", is_open=False),
            self._make_meld(Meld.KAN, honors="6666", is_open=False),
            self._make_meld(Meld.KAN, honors="7777", is_open=False),
        ]

        result = hand.estimate_hand_value(
            tiles,
            win_tile,
            melds=melds,
            dora_indicators=self._string_to_136_array(honors="22224444"),
            scores_calculator_factory=Aotenjou,
            config=self._make_hand_config(is_riichi=True,
                                          is_tsumo=True,
                                          is_ippatsu=True,
                                          is_haitei=True,
                                          player_wind=EAST,
                                          round_wind=EAST),
        )
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 95)
        self.assertEqual(result.fu, 160)
        self.assertEqual(len(result.yaku), 11)
        self.assertEqual(result.cost["main"] + result.cost["additional"],
                         101412048018258352119736256430200)
Ejemplo n.º 13
0
from mahjong.hand_calculating.hand import HandCalculator
from mahjong.tile import TilesConverter

calculator = HandCalculator()


# useful helper
def print_hand_result(hand_results):
    for result in hand_results:
        print(result)
        print('')


test_tiles = TilesConverter.string_to_72_array(tongzi='111123',
                                               tiaozi='55667788')
results = calculator.estimate_hand_value_zigong(test_tiles, 5)
print_hand_result(results)
Ejemplo n.º 14
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
Ejemplo n.º 15
0
from mahjong.hand_calculating.hand import HandCalculator
from mahjong.tile import TilesConverter
from mahjong.hand_calculating.hand_config import HandConfig
from mahjong.meld import Meld

calculator = HandCalculator()

# we had to use all 14 tiles in that array
tiles = TilesConverter.string_to_136_array(man='22444',
                                           pin='333567',
                                           sou='456')
win_tile = TilesConverter.string_to_136_array(sou='4')[0]

result = calculator.estimate_hand_value(tiles, win_tile)

print(result.han, result.fu)
print(result.cost['main'])
print(result.yaku)
for fu_item in result.fu_details:
    print(fu_item)
Ejemplo 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
Ejemplo n.º 17
0
    def tsumo(self, draw, game):
        if draw in winning_tiles(str(self.hand)):
            calculator = HandCalculator()

            dora = str(game.dora)
            if self.in_riichi:
                dora += str(game.ura_dora)

            dora_indicators = TilesConverter.one_line_string_to_136_array(
                dora, has_aka_dora=True)

            haitei = False
            tenhou = False
            chiihou = False
            if game.wall.remaining == 0:
                haitei = True
            if game.tenhou and game.wall.remaining > 65:
                if self.seat == EAST:
                    tenhou = True
                else:
                    chiihou = True

            config = HandConfig(options = OptionalRules(has_open_tanyao = True, has_aka_dora = True),\
                                is_tsumo = True,\
                                is_riichi = self.in_riichi,\
                                is_ippatsu = self.ippatsu,\
                                is_rinshan = self.rinshan,\
                                is_haitei = haitei,\
                                is_daburu_riichi = self.double_riichi,\
                                is_tenhou = tenhou,\
                                is_chiihou = chiihou,\
                                player_wind = self.seat,\
                                round_wind = game.round_wind\
                                )

            melds = []
            meld_string = ''
            for meld in self.melds.melds:
                opened = True
                if len(meld) == 4:
                    meld_type = mjMeld.KAN
                    opened = meld.opened
                elif meld.tiles.count(meld.tiles[0]) == 3:
                    meld_type = mjMeld.PON
                else:
                    meld_type = mjMeld.CHI

                    #not sure if the meld_type is necessary, but since it's not a hassle i'll leave it in
                new_meld = mjMeld(meld_type = meld_type,\
                                  tiles = TilesConverter.one_line_string_to_136_array(str(meld), has_aka_dora = True),\
                                  opened = opened)
                melds.append(new_meld)

                #really dumb workaround because the hand is supposed to be exactly 14 tiles
                #and adding the kan as a string directly to the rest of the hand
                #causes the hand to exceed 14 tiles

                #this would subtract by 4 if len(meld) referred to the length of the
                #string associated with the meld, but len(meld) actually refers
                #to the number of tiles in the meld (aka it ignores the suit)
                #so the length of '222p' is 3 and the length of '1111m' is 4
                meld_string += str(meld)[len(meld) - 3:]

            hand = TilesConverter.one_line_string_to_136_array(
                str(self.hand) + str(draw) + str(meld_string),
                has_aka_dora=True)
            draw = TilesConverter.one_line_string_to_136_array(
                str(draw), has_aka_dora=True)[0]
            result = calculator.estimate_hand_value(hand, draw, melds = melds, \
                                                    dora_indicators = dora_indicators, config = config)
            if result.yaku and result.cost and not result.error:
                return result
            else:
                return False
        else:
            return False
Ejemplo n.º 18
0
from mahjong.tile import TilesConverter
from mahjong.hand_calculating.hand import HandCalculator

hc = HandCalculator()
tc = TilesConverter()

tiles = tc.string_to_136_array('22444', '333567', '444')
result = hc.estimate_hand_value(tiles, tiles[0], None, None, None)
print(result)
Ejemplo n.º 19
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:]
Ejemplo n.º 20
0
 def erase_state(self):
     self.hand_cache_shanten = {}
     self.hand_cache_estimation = {}
     self.finished_hand = HandCalculator()
     self.grp_features = []
Ejemplo n.º 21
0
import time
from game import Hand
from game import Wall
from mahjong.hand_calculating.hand import HandCalculator, HandConfig
from mahjong.shanten import Shanten
from mahjong.tile import TilesConverter
from mahjong.agari import Agari
from mahjong.meld import Meld
import numpy as np

c = HandCalculator()
st = Shanten()
ag = Agari()

ops = 1e7

# Control Code
k = 0
cstart = time.time_ns()
while k < ops:
    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:
Ejemplo n.º 22
0
def win():
    req = flask.request.get_json()
    calculator = HandCalculator()

    try:
        tiles = TilesConverter.one_line_string_to_136_array(req["hands"],
                                                            has_aka_dora=True)
    except KeyError:
        return flask.jsonify({"error": "hands required"}), 400
    except ValueError:
        return flask.jsonify({"error": "invalid hands"}), 400

    try:
        win_tile = TilesConverter.one_line_string_to_136_array(
            req["win_tile"], has_aka_dora=True)[0]
    except KeyError:
        return flask.jsonify({"error": "win_tile required"}), 400
    except ValueError:
        return flask.jsonify({"error": "invalid win_tile"}), 400

    melds = []
    try:
        melds = req["melds"]
    except KeyError:
        pass

    meld_tiles = []
    for meld in melds:
        try:
            m = meld["meld"]
        except KeyError:
            return flask.jsonify({"error": "meld required"}), 400
        try:
            if m == "chi":
                meld_tiles.append(
                    Meld(
                        Meld.CHI,
                        TilesConverter.one_line_string_to_136_array(
                            meld["tiles"])))
            elif m == "pon":
                meld_tiles.append(
                    Meld(
                        Meld.PON,
                        TilesConverter.one_line_string_to_136_array(
                            meld["tiles"])))
            elif m == "minkan":
                meld_tiles.append(
                    Meld(
                        Meld.KAN,
                        TilesConverter.one_line_string_to_136_array(
                            meld["tiles"]), True))
            elif m == "ankan":
                meld_tiles.append(
                    Meld(
                        Meld.KAN,
                        TilesConverter.one_line_string_to_136_array(
                            meld["tiles"]), False))
            elif m == "kakan":
                meld_tiles.append(
                    Meld(
                        Meld.CHANKAN,
                        TilesConverter.one_line_string_to_136_array(
                            meld["tiles"])))
            else:
                return flask.jsonify({"error": "invalid meld"}), 400
        except KeyError:
            return flask.jsonify({"error": "meld tiles required"}), 400
        except ValueError:
            return flask.jsonify({"error": "invalid meld tiles"}), 400

    try:
        tsumo = req["tsumo"]
    except KeyError:
        tsumo = False

    try:
        riichi = req["riichi"]
    except KeyError:
        riichi = False

    try:
        ippatsu = req["ippatsu"]
    except KeyError:
        ippatsu = False

    try:
        rinshan = req["rinshan"]
    except KeyError:
        rinshan = False

    try:
        chankan = req["chankan"]
    except KeyError:
        chankan = False

    try:
        haitei = req["haitei"]
    except KeyError:
        haitei = False

    try:
        houtei = req["houtei"]
    except KeyError:
        houtei = False

    try:
        double_riichi = req["double_riichi"]
    except KeyError:
        double_riichi = False

    try:
        tenhou = req["tenhou"]
    except KeyError:
        tenhou = False

    try:
        chiihou = req["chiihou"]
    except KeyError:
        chiihou = False

    try:
        wind_player = req["wind_player"]
        if wind_player == "east":
            wind_player = EAST
        elif wind_player == "south":
            wind_player = SOUTH
        elif wind_player == "west":
            wind_player = WEST
        elif wind_player == "north":
            wind_player = NORTH
        else:
            return flask.jsonify({"error": "invalid wind_player"}), 400
    except KeyError:
        return flask.jsonify({"error": "wind_player required"}), 400

    try:
        wind_round = req["wind_round"]
        if wind_round == "east":
            wind_round = EAST
        elif wind_round == "south":
            wind_round = SOUTH
        elif wind_round == "west":
            wind_round = WEST
        elif wind_round == "north":
            wind_round = NORTH
        else:
            return flask.jsonify({"error": "invalid wind_round"}), 400
    except KeyError:
        return flask.jsonify({"error": "wind_round required"}), 400

    try:
        dora_indicators = TilesConverter.one_line_string_to_136_array(
            req["dora_indicators"], has_aka_dora=True)
    except KeyError:
        return flask.jsonify({"error": "dora_indicators required"}), 400
    except ValueError:
        return flask.jsonify({"error": "invalid dora_indicators"}), 400

    option = OptionalRules(has_open_tanyao=True,
                           has_aka_dora=True,
                           has_double_yakuman=False)

    config = HandConfig(is_tsumo=tsumo,
                        is_riichi=riichi,
                        is_ippatsu=ippatsu,
                        is_rinshan=rinshan,
                        is_chankan=chankan,
                        is_haitei=haitei,
                        is_houtei=houtei,
                        is_daburu_riichi=double_riichi,
                        is_tenhou=tenhou,
                        is_chiihou=chiihou,
                        player_wind=wind_player,
                        round_wind=wind_round,
                        options=option)

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

    if result.error:
        return flask.jsonify({"error": result.error}), 400

    yaku = map(lambda y: y.japanese, result.yaku)
    return flask.jsonify({
        "cost": result.cost,
        "han": result.han,
        "fu": result.fu,
        "yaku": list(yaku),
        "error": None
    })
Ejemplo n.º 23
0
from mahjong.tile import TilesConverter
from mahjong.hand_calculating.hand_config import HandConfig, OptionalRules
from mahjong.meld import Meld
from mahjong.constants import EAST, SOUTH, WEST, NORTH


def print_hand_result(hand_result):
    print(hand_result.han, hand_result.fu)
    print(hand_result.cost['main'], result.cost['additional'])
    print(hand_result.yaku)
    for fu_item in hand_result.fu_details:
        print(fu_item)
    print('')


calculator = HandCalculator()


class ClassCheckMahjongtiles:
    def __init__(self):
        self.Img = '5.png'
        self.ManzuInHandtile = []
        self.SozuInHandtile = []
        self.PinzuInHandtile = []
        self.ZihaiInHandtile = []

        self.ManzuTemplates = [['1', 'Templates/Manzu/p_ms1_1.png'],
                               ['2', 'Templates/Manzu/p_ms2_1.png'],
                               ['3', 'Templates/Manzu/p_ms3_1.png'],
                               ['4', 'Templates/Manzu/p_ms4_1.png'],
                               ['5', 'Templates/Manzu/p_ms5_1.png'],
Ejemplo n.º 24
0
class FeatureGenerator:
    def __init__(self):
        """
        changed the input from filename to tiles_state_and_action data
        By Jun Lin
        """
        self.shanten_calculator = Shanten()
        self.hc = HandCalculator()
        self.sc = ScoresCalculator()
        self.hand_cache_shanten = {}
        self.hand_cache_points = {}

    def build_cache_key(self, tiles_34):
        return hashlib.md5(marshal.dumps(tiles_34)).hexdigest()

    def calculate_shanten_or_get_from_cache(self, closed_hand_34):
        key = self.build_cache_key(closed_hand_34)
        if key in self.hand_cache_shanten:
            return self.hand_cache_shanten[key]
        result = self.shanten_calculator.calculate_shanten(closed_hand_34)
        self.hand_cache_shanten[key] = result
        return result

    def calculate_ponits_or_get_from_cache(self, closed_left_tiles_34,
                                           win_tile, melds, dora_indicators):
        tiles_34 = closed_left_tiles_34[:]
        for meld in melds:
            for x in meld.tiles_34:
                tiles_34[x] += 1
        key = self.build_cache_key(tiles_34 + [win_tile] + dora_indicators)
        if key in self.hand_cache_points:
            return self.hand_cache_points[key]
        # print(closed_left_tiles_34)
        # print(melds)
        hc_result = self.hc.estimate_hand_value(
            TilesConverter.to_136_array(tiles_34), win_tile, melds,
            dora_indicators)
        sc_result = self.sc.calculate_scores(hc_result.han, hc_result.fu,
                                             HandConfig(HandConstants()))
        result = sc_result["main"]
        self.hand_cache_points[key] = result
        return result

    def canwinbyreplace(self, closed_left_tiles_34, melds, dora_indicators,
                        tiles_could_draw, replacelimit):
        def _draw(closed_left_tiles_34, melds, dora_indicators,
                  tiles_could_draw, replacelimit):
            if self.calculate_shanten_or_get_from_cache(
                    closed_left_tiles_34) > replacelimit:
                return 0
            result = 0
            if replacelimit == 0:
                for idx in range(34):
                    if tiles_could_draw[idx] > 0:
                        closed_left_tiles_34[idx] += 1
                        if self.calculate_shanten_or_get_from_cache(
                                closed_left_tiles_34) == -1:
                            ponits = self.calculate_ponits_or_get_from_cache(
                                closed_left_tiles_34, idx * 4, melds,
                                dora_indicators)
                            result = max(result, ponits)
                        closed_left_tiles_34[idx] -= 1

            else:
                for idx, count in enumerate(tiles_could_draw):
                    if count > 0:
                        tiles_could_draw[idx] -= 1
                        closed_left_tiles_34[idx] += 1
                        ponits = _discard(closed_left_tiles_34, melds,
                                          dora_indicators, tiles_could_draw,
                                          replacelimit)
                        result = max(result, ponits)
                        closed_left_tiles_34[idx] -= 1
                        tiles_could_draw[idx] += 1
            return result

        def _discard(closed_left_tiles_34, melds, dora_indicators,
                     tiles_could_draw, replacelimit):
            result = 0
            for idx, count in enumerate(closed_left_tiles_34):
                if count > 0:
                    closed_left_tiles_34[idx] -= 1
                    replacelimit -= 1
                    ponits = _draw(closed_left_tiles_34, melds,
                                   dora_indicators, tiles_could_draw,
                                   replacelimit)
                    result = max(result, ponits)
                    replacelimit += 1
                    closed_left_tiles_34[idx] += 1
            return result

        return _draw(closed_left_tiles_34, melds, dora_indicators,
                     tiles_could_draw, replacelimit)

    def open_hands_detail_to_melds(self, open_hands_detail):
        melds = []
        for ohd in open_hands_detail:
            tiles = ohd["tiles"]
            if ohd["meld_type"] == "Pon":
                meld_type = "pon"
                opened = True
            elif ohd["meld_type"] == "Chi":
                meld_type = "chi"
                opened = True
            elif ohd["meld_type"] == "AnKan":
                meld_type = "kan"
                opened = False
            else:
                meld_type = "kan"
                opened = True
            meld = Meld(meld_type, tiles, opened)
            melds.append(meld)
        return melds

    def getPlayerTiles(self, player_tiles):
        closed_hand_136 = player_tiles.get('closed_hand:', [])
        open_hand_136 = player_tiles.get('open_hand', [])
        discarded_tiles_136 = player_tiles.get('discarded_tiles', [])

        closed_hand_feature = np.zeros((4, 34))
        open_hand_feature = np.zeros((4, 34))
        discarded_tiles_feature = np.zeros((4, 34))

        for val in closed_hand_136:
            idx = 0
            while (closed_hand_feature[idx][val // 4] == 1):
                idx += 1
            closed_hand_feature[idx][val // 4] = 1
        for val in open_hand_136:
            idx = 0
            while (open_hand_feature[idx][val // 4] == 1):
                idx += 1
            open_hand_feature[idx][val // 4] = 1
        for val in discarded_tiles_136:
            idx = 0
            while (discarded_tiles_feature[idx][val // 4] == 1):
                idx += 1
            discarded_tiles_feature[idx][val // 4] = 1

        return np.concatenate(
            (closed_hand_feature, open_hand_feature, discarded_tiles_feature))

    def getSelfTiles(self, tiles_state_and_action):
        player_tiles = tiles_state_and_action["player_tiles"]
        return self.getPlayerTiles(player_tiles)

    def getEnemiesTiles(self, tiles_state_and_action):
        player_seat = tiles_state_and_action["player_id"]
        enemies_tiles_list = tiles_state_and_action["enemies_tiles"]
        if (len(enemies_tiles_list) == 3):
            enemies_tiles_list.insert(player_seat,
                                      tiles_state_and_action["player_tiles"])
        enemies_tiles_feature = np.empty((0, 34))
        for i in range(3):
            player_seat = (player_seat + 1) % 4
            player_tiles = self.getPlayerTiles(enemies_tiles_list[player_seat])
            enemies_tiles_feature = np.concatenate(
                (enemies_tiles_feature, player_tiles))
        return enemies_tiles_feature

    def getDoraIndicatorList(self, tiles_state_and_action):
        dora_indicator_list = tiles_state_and_action["dora"]
        dora_indicator_feature = np.zeros((5, 34))
        for idx, val in enumerate(dora_indicator_list):
            dora_indicator_feature[idx][val // 4] = 1
        return dora_indicator_feature

    def getDoraList(self, tiles_state_and_action):
        def indicator2dora(dora_indicator):
            dora = dora_indicator // 4
            if dora < 27:  # EAST
                if dora == 8:
                    dora = -1
                elif dora == 17:
                    dora = 8
                elif dora == 26:
                    dora = 17
            else:
                dora -= 9 * 3
                if dora == 3:
                    dora = -1
                elif dora == 6:
                    dora = 3
                dora += 9 * 3
            dora += 1
            return dora

        dora_indicator_list = tiles_state_and_action["dora"]
        dora_feature = np.zeros((5, 34))
        for idx, dora_indicator in enumerate(dora_indicator_list):
            dora_feature[idx][indicator2dora(dora_indicator)] = 1
        return dora_feature

    def getScoreList1(self, tiles_state_and_action):
        def trans_score(score):
            feature = np.zeros((34))
            if score > 50000:
                score = 50000
            a = score // (50000.0 / 33)
            # alpha = a + 1 - (score*33.0/50000)
            # alpha = round(alpha, 2)
            # feature[int(a)] = alpha
            # if a<33:
            #     feature[int(a+1)] = 1-alpha
            feature[int(a)] = 1
            return feature

        player_seat = tiles_state_and_action["player_id"]
        scores_list = tiles_state_and_action["scores"]
        scores_feature = np.zeros((4, 34))
        for i in range(4):
            score = scores_list[player_seat]
            scores_feature[i] = trans_score(score)
            player_seat += 1
            player_seat %= 4
        return scores_feature

    def getBoard1(self, tiles_state_and_action):
        def wind(c):
            if c == 'E':
                return 27
            if c == 'S':
                return 28
            if c == 'W':
                return 29
            if c == 'N':
                return 30

        player_seat = tiles_state_and_action["player_id"]
        dealer_seat = tiles_state_and_action["dealer"]
        repeat_dealer = tiles_state_and_action["repeat_dealer"]
        riichi_bets = tiles_state_and_action["riichi_bets"]
        player_wind = tiles_state_and_action["player_wind"]
        prevailing_wind = tiles_state_and_action["prevailing_wind"]

        dealer_feature = np.zeros((1, 34))
        dealer_feature[0][(dealer_seat + 4 - player_seat) % 4] = 1

        repeat_dealer_feature = np.zeros((1, 34))
        repeat_dealer_feature[0][repeat_dealer] = 1

        riichi_bets_feature = np.zeros((1, 34))
        riichi_bets_feature[0][riichi_bets] = 1

        player_wind_feature = np.zeros((1, 34))
        player_wind_feature[0][wind(player_wind)] = 1

        prevailing_wind_feature = np.zeros((1, 34))
        prevailing_wind_feature[0][wind(prevailing_wind)] = 1

        return np.concatenate(
            (dealer_feature, repeat_dealer_feature, riichi_bets_feature,
             player_wind_feature, prevailing_wind_feature))

    def getLookAheadFeature(self, tiles_state_and_action):
        # 0 for whether can be discarded
        # 1 2 3 for shanten
        # 4 5 6 7 for whether can get 2k 4k 6k 8k points with replacing 3 tiles ---- need review: takes too long!
        # 8 9 10 for in Shimocha Toimen Kamicha discarded
        # lookAheadFeature = np.zeros((11, 34))
        player_tiles = tiles_state_and_action["player_tiles"]
        closed_hand_136 = player_tiles.get('closed_hand:', [])
        open_hands_detail = tiles_state_and_action["open_hands_detail"]
        melds = self.open_hands_detail_to_melds(open_hands_detail)
        discarded_tiles_136 = player_tiles.get('discarded_tiles', [])
        player_seat = tiles_state_and_action["player_id"]
        enemies_tiles_list = tiles_state_and_action["enemies_tiles"]
        dora_indicators = tiles_state_and_action["dora"]
        if (len(enemies_tiles_list) == 3):
            enemies_tiles_list.insert(player_seat, player_tiles)
        tiles_could_draw = np.ones(34) * 4
        for player in enemies_tiles_list:
            for tile_set in [
                    player.get('closed_hand:', []),
                    player.get('open_hand', []),
                    player.get('discarded_tiles', [])
            ]:
                for tile in tile_set:
                    tiles_could_draw[tile // 4] -= 1
        for dora_tile in dora_indicators:
            tiles_could_draw[tile // 4] -= 1

        def feature_process(i, closed_hand_136, melds, dora_indicators,
                            tiles_could_draw, player_seat, enemies_tiles_list):
            feature = np.zeros((11, 1))
            discard_tiles = [
                x for x in [i * 4, i * 4 + 1, i * 4 + 2, i * 4 + 3]
                if x in closed_hand_136
            ]
            if len(discard_tiles) != 0:
                discard_tile = discard_tiles[0]
                feature[0] = 1
                closed_left_tiles_34 = TilesConverter.to_34_array(
                    [t for t in closed_hand_136 if t != discard_tile])
                shanten = self.calculate_shanten_or_get_from_cache(
                    closed_left_tiles_34)
                for i in range(3):
                    if shanten <= i:
                        feature[i + 1] = 1
                maxscore = 0  #self.canwinbyreplace(closed_left_tiles_34, melds, dora_indicators,
                #tiles_could_draw, replacelimit = 2)
                scores = [2000, 4000, 6000, 8000]
                for i in range(4):
                    if maxscore >= scores[i]:
                        feature[i + 4] = 1
                seat = player_seat
                for i in range(3):
                    seat = (seat + 1) % 4
                    if discard_tile // 4 in [
                            t // 4 for t in enemies_tiles_list[seat].get(
                                'discarded_tiles', [])
                    ]:
                        feature[i + 8] = 1
            return feature

        results = Parallel(n_jobs=8)(delayed(
            feature_process)(i, closed_hand_136, melds, dora_indicators,
                             tiles_could_draw, player_seat, enemies_tiles_list)
                                     for i in range(34))
        return np.concatenate(results, axis=1)

    def getGeneralFeature(self, tiles_state_and_action):
        return np.concatenate((
            #self.getLookAheadFeature(tiles_state_and_action), #(11,34)
            self.getSelfTiles(tiles_state_and_action),  # (12,34)
            self.getDoraList(tiles_state_and_action),  # (5,34)
            self.getBoard1(tiles_state_and_action),  # (5,34)
            self.getEnemiesTiles(tiles_state_and_action),  # (36,34)
            self.getScoreList1(tiles_state_and_action)  # (4,34)
        ))

    def ChiFeatureGenerator(self, tiles_state_and_action):
        """
        changed the input from filename to tiles_state_and_action data
        By Jun Lin
        """
        def _chilist(last_player_discarded_tile, closed_hand_136):
            res = []
            last_player_discarded_tile_34 = last_player_discarded_tile // 4
            tile_set = last_player_discarded_tile_34 // 9
            if tile_set == 3:
                return res
            closed_hand_34_detail = [[] for i in range(34)]
            for tile in closed_hand_136:
                closed_hand_34_detail[tile // 4].append(tile)
            if last_player_discarded_tile_34 > tile_set * 9 + 1:
                if len(closed_hand_34_detail[last_player_discarded_tile_34 - 1]
                       ) != 0 and len(closed_hand_34_detail[
                           last_player_discarded_tile_34 - 2]) != 0:
                    res.append([
                        closed_hand_34_detail[last_player_discarded_tile_34 -
                                              1][0],
                        closed_hand_34_detail[last_player_discarded_tile_34 -
                                              2][0], last_player_discarded_tile
                    ])
            if last_player_discarded_tile_34 > tile_set * 9 and last_player_discarded_tile_34 < tile_set * 9 + 8:
                if len(closed_hand_34_detail[last_player_discarded_tile_34 - 1]
                       ) != 0 and len(closed_hand_34_detail[
                           last_player_discarded_tile_34 + 1]) != 0:
                    res.append([
                        closed_hand_34_detail[last_player_discarded_tile_34 -
                                              1][0],
                        closed_hand_34_detail[last_player_discarded_tile_34 +
                                              1][0], last_player_discarded_tile
                    ])
            if last_player_discarded_tile_34 < tile_set * 9 + 7:
                if len(closed_hand_34_detail[last_player_discarded_tile_34 + 1]
                       ) != 0 and len(closed_hand_34_detail[
                           last_player_discarded_tile_34 + 2]) != 0:
                    res.append([
                        closed_hand_34_detail[last_player_discarded_tile_34 +
                                              1][0],
                        closed_hand_34_detail[last_player_discarded_tile_34 +
                                              2][0], last_player_discarded_tile
                    ])
            return res
            # res = []
            # pairs = [(a, b) for idx, a in enumerate(closed_hand_136) for b in closed_hand_136[idx + 1:]]
            # for p in pairs:
            #     meld = [last_player_discarded_tile,p[0],p[1]]
            #     if all(tile < 36 for tile in meld) or all(36 <= tile < 72 for tile in meld) or all(36 <= tile < 72 for tile in meld):
            #         meld34 = [tile//4 for tile in meld]
            #         if any(tile+1 in meld34 and tile-1 in meld34 for tile in meld34):
            #             res.append(meld)
            # return res

        could_chi = tiles_state_and_action["could_chi"]
        last_player_discarded_tile = tiles_state_and_action[
            "last_player_discarded_tile"]
        closed_hand_136 = tiles_state_and_action["player_tiles"][
            'closed_hand:']
        action = tiles_state_and_action["action"]
        if could_chi == 1:
            generalFeature = self.getGeneralFeature(tiles_state_and_action)
            if action[0] == 'Chi':
                print(_chilist(last_player_discarded_tile, closed_hand_136))
                print(action)
            for chimeld in _chilist(last_player_discarded_tile,
                                    closed_hand_136):
                last_player_discarded_tile_feature = np.zeros((1, 34))
                for chitile in chimeld:
                    last_player_discarded_tile_feature[0][chitile // 4] = 1
                x = np.concatenate(
                    (last_player_discarded_tile_feature, generalFeature))
                if action[0] == 'Chi' and all(
                        chitile // 4 in [a // 4 for a in action[1]]
                        for chitile in chimeld):
                    y = 1
                else:
                    y = 0
                # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)),
                #        "labels": to_categorical(y, num_classes=2)}
                yield x.reshape(
                    (x.shape[0], x.shape[1], 1)), to_categorical(y,
                                                                 num_classes=2)

    def PonFeatureGenerator(self, tiles_state_and_action):
        """
        changed the input from filename to tiles_state_and_action data
        By Jun Lin
        """
        last_discarded_tile = tiles_state_and_action[
            "last_player_discarded_tile"]
        closed_hand_136 = tiles_state_and_action["player_tiles"][
            'closed_hand:']
        action = tiles_state_and_action["action"]
        if tiles_state_and_action["could_pon"] == 1:
            last_discarded_tile_feature = np.zeros((1, 34))
            last_discarded_tile_feature[0][last_discarded_tile // 4] = 1
            x = np.concatenate(
                (last_discarded_tile_feature,
                 self.getGeneralFeature(tiles_state_and_action)))
            if action[0] == 'Pon':
                y = 1
            else:
                y = 0
            # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)),
            #        "labels": to_categorical(y, num_classes=2)}
            yield x.reshape(
                (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2)

    def KanFeatureGenerator(self, tiles_state_and_action):
        """
        changed the input from filename to tiles_state_and_action data
        By Jun Lin
        """
        def could_ankan(closed_hand_136):
            count = np.zeros(34)
            for tile in closed_hand_136:
                count[tile // 4] += 1
                if count[tile // 4] == 4:
                    return True
            return False

        def could_kakan(closed_hand_136, open_hand_136):
            count = np.zeros(34)
            for tile in open_hand_136:
                count[tile // 4] += 1
            for tile in closed_hand_136:
                if count[tile // 4] == 3:
                    return True
            return False

        last_discarded_tile = tiles_state_and_action[
            "last_player_discarded_tile"]
        closed_hand_136 = tiles_state_and_action["player_tiles"][
            'closed_hand:']
        open_hand_136 = tiles_state_and_action["player_tiles"]['open_hand']
        action = tiles_state_and_action["action"]
        if tiles_state_and_action["could_minkan"] == 1:  # Minkan
            kan_type_feature = np.zeros((3, 34))
            kan_type_feature[0] = 1
            last_discarded_tile_feature = np.zeros((1, 34))
            last_discarded_tile_feature[0][last_discarded_tile // 4] = 1
            x = np.concatenate(
                (kan_type_feature, last_discarded_tile_feature,
                 self.getGeneralFeature(tiles_state_and_action)))

            if action[0] == 'MinKan' and (last_discarded_tile in action[1]):
                y = 1
            else:
                y = 0
            # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)),
            #        "labels": to_categorical(y, num_classes=2)}
            yield x.reshape(
                (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2)
        else:
            if could_ankan(closed_hand_136):  # AnKan
                kan_type_feature = np.zeros((3, 34))
                kan_type_feature[1] = 1
                last_discarded_tile_feature = np.zeros((1, 34))
                x = np.concatenate(
                    (kan_type_feature, last_discarded_tile_feature,
                     self.getGeneralFeature(tiles_state_and_action)))

                if action[0] == 'AnKan':
                    y = 1
                else:
                    y = 0
                # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)),
                #        "labels": to_categorical(y, num_classes=2)}
                yield x.reshape(
                    (x.shape[0], x.shape[1], 1)), to_categorical(y,
                                                                 num_classes=2)
            else:
                if could_kakan(closed_hand_136, open_hand_136):  # KaKan
                    kan_type_feature = np.zeros((3, 34))
                    kan_type_feature[2] = 1
                    last_discarded_tile_feature = np.zeros((1, 34))
                    x = np.concatenate(
                        (kan_type_feature, last_discarded_tile_feature,
                         self.getGeneralFeature(tiles_state_and_action)))
                    if action[0] == 'KaKan':
                        y = 1
                    else:
                        y = 0
                    # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)),
                    #        "labels": to_categorical(y, num_classes=2)}
                    yield x.reshape((x.shape[0], x.shape[1],
                                     1)), to_categorical(y, num_classes=2)

    def RiichiFeatureGenerator(self, tiles_state_and_action):
        action = tiles_state_and_action["action"]
        if tiles_state_and_action["is_FCH"] == 1:
            tiles_34 = TilesConverter.to_34_array(
                tiles_state_and_action["player_tiles"]["closed_hand:"])
            # min_shanten = self.shanten_calculator.calculate_shanten(tiles_34)
            min_shanten = self.calculate_shanten_or_get_from_cache(tiles_34)
            if min_shanten == 0:
                x = self.getGeneralFeature(tiles_state_and_action)
                if action[0] == 'REACH':
                    y = 1
                else:
                    y = 0
                # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)),
                #        "labels": to_categorical(y, num_classes=2)}
                yield x.reshape(
                    (x.shape[0], x.shape[1], 1)), to_categorical(y,
                                                                 num_classes=2)

    def DiscardFeatureGenerator(self, tiles_state_and_action):
        x = self.getGeneralFeature(tiles_state_and_action)
        y = tiles_state_and_action["discarded_tile"] // 4
        yield x.reshape(
            (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=34)
Ejemplo n.º 25
0
def score(boxes, tsumo, y_divider, round_wind=None, player_wind=None):
    """
    +---------------+
    |  _ _ _ _   _  |
    | |_|_|_|_| |_| | classfied to `open`
    |               |
    |---------------| <- y_divider
    |  _ _ _ _ _ _  |
    | |_|_|_|_|_|_| | classfied to `closed`
    |               |
    +---------------+
    boxes:
      array of box which elements are: tile id, xmin, ymin, xmax and ymax.
      e.g. [[0, 100, 200, 150, 250],
            [2, 200, 300, 350, 350],
            [...]]
      the right most of the tile is assumed as win tile, and ron or tsumo is specified by tsumo.
      tile id is:
        man1...man9: 0...8
        pin1...pin9: 9...17
        sou1...sou9: 18...26
        ton, nan, sha, pei: 27, 28, 29, 30
        haku, hatsu, chun: 31, 32, 33
    y_divider:
      tiles are divided into two types; open or closed.
      if the position is upper than y_divider, the tile is classfied as open.
    """
    open_boxes, win_box, closed_boxes = classify_boxes(boxes, y_divider)
    if open_boxes is None:
        return None

    melds = make_melds(open_boxes)
    if melds is None:
        return None

    # 4-tile-set of kan should be trimed to 3-tile-set.
    open_kan = [
        x.tiles_34[0] for x in melds if x.type == Meld.KAN and x.opened
    ]
    open_tiles = boxes_to_tiles(open_boxes, open_kan)
    if open_tiles is None:
        return None

    win_tile = boxes_to_tiles([win_box])
    if win_tile is None:
        return None

    closed_kan_melds, closed_boxes_replaced = make_closed_kan_melds(
        closed_boxes)

    # 4-tile-set of kan should be trimed to 3-tile-set.
    closed_kan = [x.tiles_34[0] for x in closed_kan_melds]
    closed_tiles = boxes_to_tiles(closed_boxes_replaced, closed_kan)
    if closed_tiles is None:
        return None

    hand = closed_tiles + open_tiles + win_tile
    print("win:\n", tc.to_one_line_string(win_tile))
    print("hand:\n", tc.to_one_line_string(hand))
    print("melds:\n", melds + closed_kan_melds)
    for meld in melds:
        print(tc.to_one_line_string(meld.tiles))

    options = OptionalRules(has_open_tanyao=True,
                            kazoe_limit=HandConfig.KAZOE_NO_LIMIT)
    config = HandConfig(
        is_tsumo=tsumo,
        player_wind=player_wind,
        round_wind=round_wind,
        options=options,
    )
    print("tsumo:", tsumo, " player:", player_wind, " round:", round_wind)

    result = HandCalculator().estimate_hand_value(hand,
                                                  win_tile[0],
                                                  melds=melds +
                                                  closed_kan_melds,
                                                  config=config)
    print_hand_result(result)
    return result
Ejemplo n.º 26
0
class MahjongAI:
    version = "0.5.1"

    agari = None
    shanten_calculator = None
    defence = None
    riichi = None
    hand_divider = None
    finished_hand = None

    shanten = 7
    ukeire = 0
    ukeire_second = 0
    waiting = None

    current_strategy = None
    last_discard_option = None

    hand_cache_shanten = {}
    hand_cache_estimation = {}

    def __init__(self, player):
        self.player = player
        self.table = player.table

        self.kan = Kan(player)
        self.agari = Agari()
        self.shanten_calculator = Shanten()
        self.defence = TileDangerHandler(player)
        self.riichi = Riichi(player)
        self.hand_divider = HandDivider()
        self.finished_hand = HandCalculator()
        self.hand_builder = HandBuilder(player, self)
        self.placement = player.config.PLACEMENT_HANDLER_CLASS(player)

        self.suji = Suji(player)
        self.kabe = Kabe(player)

        self.erase_state()

    def erase_state(self):
        self.shanten = 7
        self.ukeire = 0
        self.ukeire_second = 0
        self.waiting = None

        self.current_strategy = None
        self.last_discard_option = None

        self.hand_cache_shanten = {}
        self.hand_cache_estimation = {}

        # to erase hand cache
        self.finished_hand = HandCalculator()

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

        self.shanten, _ = self.hand_builder.calculate_shanten_and_decide_hand_structure(
            TilesConverter.to_34_array(self.player.tiles))

    def draw_tile(self, tile_136):
        if not self.player.in_riichi:
            self.determine_strategy(self.player.tiles)

    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, False

            return self.hand_builder.process_discard_option(
                self.last_discard_option)

        return self.hand_builder.discard_tile()

    def try_to_call_meld(self, tile_136, is_kamicha_discard, meld_type=None):
        tiles_136_previous = self.player.tiles[:]
        closed_hand_136_previous = self.player.closed_hand[:]
        tiles_136 = tiles_136_previous + [tile_136]
        self.determine_strategy(tiles_136, meld_tile=tile_136)

        if not self.current_strategy:
            self.player.logger.debug(
                log.MELD_DEBUG,
                "We don't have active strategy. Abort melding.")
            return None, None

        closed_hand_34_previous = TilesConverter.to_34_array(
            closed_hand_136_previous)
        previous_shanten, _ = self.hand_builder.calculate_shanten_and_decide_hand_structure(
            closed_hand_34_previous)

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

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

            self.player.logger.debug(
                log.MELD_CALL,
                "We decided to open hand",
                context=[
                    f"Hand: {self.player.format_hand_for_print(tile_136)}",
                    f"Meld: {meld.serialize()}",
                    f"Discard after meld: {discard_option.serialize()}",
                ],
            )

        return meld, discard_option

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

        old_strategy = self.current_strategy
        self.current_strategy = None

        # order is important, we add strategies with the highest priority first
        strategies = []

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

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

        strategies.append(
            FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player))
        strategies.append(
            CommonOpenTempaiStrategy(BaseStrategy.COMMON_OPEN_TEMPAI,
                                     self.player))

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

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

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

        return self.current_strategy and True or False

    def estimate_hand_value_or_get_from_cache(self,
                                              win_tile_34,
                                              tiles=None,
                                              call_riichi=False,
                                              is_tsumo=False,
                                              is_rinshan=False,
                                              is_chankan=False):
        win_tile_136 = win_tile_34 * 4

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

        if not tiles:
            tiles = self.player.tiles[:]
        else:
            tiles = tiles[:]

        tiles += [win_tile_136]

        config = HandConfig(
            is_riichi=call_riichi,
            player_wind=self.player.player_wind,
            round_wind=self.player.table.round_wind_tile,
            is_tsumo=is_tsumo,
            is_rinshan=is_rinshan,
            is_chankan=is_chankan,
            options=OptionalRules(
                has_aka_dora=self.player.table.has_aka_dora,
                has_open_tanyao=self.player.table.has_open_tanyao,
                has_double_yakuman=False,
            ),
            tsumi_number=self.player.table.count_of_honba_sticks,
            kyoutaku_number=self.player.table.count_of_riichi_sticks,
        )

        return self._estimate_hand_value_or_get_from_cache(
            win_tile_136, tiles, call_riichi, is_tsumo, 0, config, is_rinshan,
            is_chankan)

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

        tiles += [win_tile_136]

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

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

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

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

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

        self.hand_cache_estimation[cache_key] = result
        return result

    def estimate_weighted_mean_hand_value(self, discard_option):
        weighted_hand_cost = 0
        number_of_tiles = 0
        for waiting in discard_option.waiting:
            tiles = self.player.tiles[:]
            tiles.remove(discard_option.tile_to_discard_136)

            hand_cost = self.estimate_hand_value_or_get_from_cache(
                waiting,
                tiles=tiles,
                call_riichi=discard_option.with_riichi,
                is_tsumo=True)

            if not hand_cost.cost:
                continue

            weighted_hand_cost += (hand_cost.cost["main"] +
                                   2 * hand_cost.cost["additional"]
                                   ) * discard_option.wait_to_ukeire[waiting]
            number_of_tiles += discard_option.wait_to_ukeire[waiting]

        cost = number_of_tiles and int(
            weighted_hand_cost / number_of_tiles) or 0

        # we are karaten, or we don't have yaku
        # in that case let's add possible tempai cost
        if cost == 0 and self.player.round_step > 12:
            cost = 1000

        if self.player.round_step > 15 and cost < 2500:
            cost = 2500

        return cost

    def should_call_kyuushu_kyuuhai(self) -> bool:
        """
        Kyuushu kyuuhai 「九種九牌」
        (9 kinds of honor or terminal tiles)
        """
        # TODO aim for kokushi
        return True

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

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

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

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

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

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

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

    @property
    def enemy_players(self):
        """
        Return list of players except our bot
        """
        return self.player.table.players[1:]
Ejemplo n.º 27
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:]
Ejemplo n.º 28
0
class MonteCarlo(object):
    """A simulator to run Monte Carlo simulation on Mahjong game
    """
    decoder = TenhouDecoder()
    agari = Agari()
    finished_hand = FinishedHand()
    hand_calculator = HandCalculator()
    verbose = False

    def __init__(self):

        previous_ai = False
        table = MCVisibleTable(previous_ai)
        self.table = table

        self.initialize()

        # We load the classifiers and regressors here
        self.clf_one_player = []
        for n in range(34):
            clf = pickle.load(
                open(
                    abs_data_path +
                    "/train_model/trained_models/one_player_{}.sav".format(n),
                    "rb"))
            self.clf_one_player.append(clf)

    def initialize(self):
        # Mahjong table is ready
        #        previous_ai = False
        #        table = MCVisibleTable(previous_ai)
        #        self.table = table
        self.table.erase_state()
        self.table.turn_number = [0, 0, 0, 0]

        # Prepare a new deck of mahjong tiles
        tiles = list(range(136))
        # shuffle the tiles
        random.shuffle(tiles)
        # seperate dead wall and dora indicator
        dead_wall = tiles[-14:]
        dora_indicator = dead_wall[4]  # random.choice(dead_wall)
        self.table.dead_wall = dead_wall
        # generate init hands for 4 players
        tiles = tiles[0:-14]
        hands = []
        for n in range(4):
            init_tiles = tiles[0:13]
            hands.append(init_tiles)
            tiles = tiles[13:]

        self.table.remaining_tiles = tiles

        # formulate the init message
        dice1 = random.choice([1, 2, 3, 4, 5, 6])
        dice2 = random.choice([1, 2, 3, 4, 5, 6])
        dealer = (dice1 - 1 + dice2 - 1) % 4
        message = "<INIT "
        # TODO: You may need round number, number of combo sticks, and number of
        # riichi sticks other than 0?
        message += 'seed="{},{},{},{},{},{}" '.format(0, 0, 0, dice1 - 1,
                                                      dice2 - 1,
                                                      dora_indicator)
        message += 'ten="250,250,250,250" '
        message += 'oya="{}" '.format(dealer)
        message += 'hai0="{}" '.format(",".join(str(t) for t in hands[0]))
        message += 'hai1="{}" '.format(",".join(str(t) for t in hands[1]))
        message += 'hai2="{}" '.format(",".join(str(t) for t in hands[2]))
        message += 'hai3="{}"/>'.format(",".join(str(t) for t in hands[3]))

        # once message is ready, we decoder the info and initialize the table
        values = self.decoder.parse_initial_values(message)
        self.table.init_round(
            values['round_number'],
            values['count_of_honba_sticks'],
            values['count_of_riichi_sticks'],
            values['dora_indicator'],
            values['dealer'],
            values['scores'],
        )
        # TODO: this part is unnecessary, since we have already the hands list
        hands = [
            [
                int(x) for x in self.decoder.get_attribute_content(
                    message, 'hai0').split(',')
            ],
            [
                int(x) for x in self.decoder.get_attribute_content(
                    message, 'hai1').split(',')
            ],
            [
                int(x) for x in self.decoder.get_attribute_content(
                    message, 'hai2').split(',')
            ],
            [
                int(x) for x in self.decoder.get_attribute_content(
                    message, 'hai3').split(',')
            ],
        ]
        # Initialize all players on the table
        # TODO: ok, we always assume we are sitting at seat 0
        self.player_position = 0
        self.table.players[0].init_hand(hands[self.player_position])
        self.table.players[1].init_hand(hands[(self.player_position + 1) % 4])
        self.table.players[2].init_hand(hands[(self.player_position + 2) % 4])
        self.table.players[3].init_hand(hands[(self.player_position + 3) % 4])
        #        main_player = self.table.player
        #        print(self.table.__str__())
        #        print('Players: {}'.format(self.table.get_players_sorted_by_scores()))
        #        print('Dealer: {}'.format(self.table.get_player(values['dealer'])))
        #        print('Round  wind: {}'.format(DISPLAY_WINDS[self.table.round_wind]))
        #        print('Player wind: {}'.format(DISPLAY_WINDS[main_player.player_wind]))

        # TODO: this part may not be necessary. Basically we need to erase the
        # melds information since it is a new game.
        # If we are using opponent_model, we need to reset the melds
        # when a new game initiated. For other model, this function
        # may not exist. [Joseph]
        try:
            self.table.player.ai.reset_melds()
        except:
            pass

    def _check_win(self, player_hand, melds):
        """ check the hand to see whether it's a win hand or not
        :param player_hand: list of int (0-33), the player's hand tiles in 34 format
        :return: True if win, else False.
        """
        return self.agari.is_agari(player_hand, melds)

    def check_win(self, p):
        """check win hand
        :param p: int (0-3), player index
        :return: True if win, False if not.
        """
        player_hand = self.table.players[p].tiles
        player_hand_34 = TilesConverter.to_34_array(player_hand)
        melds = [meld.tiles for meld in self.table.players[p].melds
                 ]  # self.table.players[p].melds is [list of Meld()]
        melds_34 = list(map(lambda lst: [l // 4 for l in lst], melds))
        return self._check_win(player_hand_34, melds_34)

    def _check_waiting(self, player_hand):
        """Check whether a player is waiting or not based on his hand
        :param player_hand: list of elements in [0,133], the hand tiles of player
        :return: True for waiting, False for not waiting
        """
        # If we need one more tile to complete our hand, and this specific tile
        # we want is known to be within the wall tiles, then the hand is waiting.
        current_hand = TilesConverter.to_34_array(player_hand)
        #winning_tiles = []
        for n in range(34):
            table_revealed_tiles_34 = self.table.revealed_tiles
            if table_revealed_tiles_34[n] < 4:
                completed_hand = current_hand[:]
                completed_hand[n] += 1
                can_be_waiting = self.agari.is_agari(completed_hand)
                if can_be_waiting:
                    return True
                    #winning_tiles.append(n) # n is the winning tile we want

        # If there exists winnint tiles, the player is waitinng
        #if len(winning_tiles)>0:
        #    return True
        return False

    def check_waiting(self, p):
        """check whether the player `p` is waiting or not
        :param p: int (0-3), player index
        :return: True for waiting, False for not waiting
        """
        player_hand = self.table.players[p].tiles
        return self._check_waiting(player_hand)

    def discard_tile(self, p):
        """choose a tile to discard based on hand tiles of player `p`
        :param p: int(0-3), player index
        :return: int(0-135), tile to discard
        """
        tiles = self.table.players[p].tiles
        closed_hand = self.table.players[p].closed_hand
        open_sets_34 = self.table.players[p].open_hand_34_tiles
        table_revealed_tiles_34 = self.table.revealed_tiles
        discarded_tile = self.table.players[p].ai.discard_tile(
            tiles, closed_hand, open_sets_34, table_revealed_tiles_34)
        return discarded_tile

    def call_meld(self, type, who, from_who, opened, tiles, called_tile):
        meld = Meld()
        meld.type = type
        meld.who = who
        meld.from_who = from_who
        meld.opened = opened
        meld.tiles = tiles
        meld.called_tile = called_tile
        return meld

    def sim_WPET(self):
        """Simulation of a riichi mahjong game, and record the bw and r parameters
        """
        # We don't need to fix the seed now.
        #seed = random.randint(1,100000)
        #seed = 73823
        #random.seed(seed)

        bw_riichi = 0
        bw_stealing = 0
        r = 0

        # start a new game
        self.initialize()
        # rinshan
        rinshan = self.table.dead_wall[0:4]
        # start from dealer
        p = self.table.dealer_seat
        #logger.info("seed: %d"%seed)
        logger.info("dealer seat: %d" % p)
        # Fixed: self.table.count_of_remaining_tiles>0 may not be accurate here
        while self.table.count_of_remaining_tiles > 0:  #len(self.table.remaining_tiles)>0:

            # our perspective of view is from the program's turn
            if p == 0:
                r += 1

            if self.check_waiting(0):
                # TODO: Why don't we use in_riichi here?
                if self.table.players[0].is_open_hand:
                    bw_stealing = 1
                else:
                    bw_riichi = 1
                break

            logger.debug("[begin]:%s,%s" %
                         (self.table.count_of_remaining_tiles,
                          len(self.table.remaining_tiles)))
            # p draw a tile
            drawn_tile = self.table.remaining_tiles[0]
            self.table.players[p].draw_tile(drawn_tile)
            # check p win
            if self.check_win(p):
                logger.info("player {} wins (tsumo)!".format(p))
                break

            # TODO: we only apply discard_tile strategy to our program, for the
            # opponent players, we assume they simply discard what they draw
            # if not win, we do the followings
            if True:  #p==0:
                if not self.table.players[p].in_riichi:
                    # choose a discard tile for playe `p`
                    discarded_tile = self.discard_tile(p)
                    # see if we can call riichi
                    if self.table.players[p].can_call_riichi():
                        logger.debug("player {} call riichi.".format(p))
                        self.table.players[p].in_riichi = True
                else:  # if riichi, we have to discard whatever we draw
                    discarded_tile = drawn_tile
            else:
                discarded_tile = drawn_tile

            # remove the tile from player's hand
            discarded_tile_tmp = discarded_tile
            drawn_tile_tmp = drawn_tile
            is_tsumogiri = (discarded_tile == drawn_tile)
            self.table.players[p].tiles.remove(discarded_tile)
            logger.debug("player {} discards {}".format(p, discarded_tile))
            logger.debug("\tclosed hand: %s" %
                         self.table.players[0].closed_hand)
            logger.debug("\topen hand: %s" %
                         self.table.players[0].open_hand_34_tiles)
            logger.debug("\tmeld tiles: %s" % self.table.players[0].meld_tiles)
            # now we check call meld
            # TODO: Ok, here we only allow our program to call meld. But maybe
            # we should allow the opponents to call meld too?
            if p != 0:
                previous_drawn_tile = drawn_tile
                tile = discarded_tile
                is_kamicha_discard = (p == 3)
                meld, discard_option = self.table.players[0].try_to_call_meld(
                    tile, is_kamicha_discard)
                kan_type = self.table.players[0].can_call_kan(tile, True)
                if kan_type:  # kan or chankan
                    tiles = [(tile // 4) * 4, (tile // 4) * 4 + 1,
                             (tile // 4) * 4 + 2, (tile // 4) * 4 + 3]
                    meld = self.call_meld(kan_type, 0, p, True, tiles, tile)
                    logger.debug("player 0 call kan from %d: %s" % (p, meld))
                    player_seat = meld.who
                    self.table.add_called_meld(player_seat, meld)
                    # we had to delete called tile from hand
                    # to have correct tiles count in the hand
                    # TODO[joseph]: I still don't know why we need this
                    # if meld type is: chi, pon, or nuki
                    # maybe no use here, b/c the meld type is always Meld.KAN here
                    if meld.type != Meld.KAN and meld.type != Meld.CHANKAN:
                        self.table.players[player_seat].draw_tile(
                            meld.called_tile)
                    # draw a tile from dead wall
                    if len(rinshan) > 0:
                        drawn_tile = rinshan.pop(0)
                        self.table.players[0].draw_tile(drawn_tile)
                        # check p win
                        if self.check_win(0):
                            logger.info(
                                "player {} wins (rinshan tsumo)!".format(0))
                            break
                        # if not win, we do the followings
                        if not self.table.players[0].in_riichi:
                            # choose a discard tile for playe `p`
                            discarded_tile = self.discard_tile(0)
                            # see if we can call riichi
                            if self.table.players[0].can_call_riichi():
                                logger.info("player {} call riichi.".format(0))
                                self.table.players[0].in_riichi = True
                        else:  # if riichi, we have to discard whatever we draw
                            discarded_tile = drawn_tile
                        # remove the tile from player's hand
                        self.table.players[0].tiles.remove(discarded_tile)
                        logger.debug("player {} discards {} after kan".format(
                            0, discarded_tile))
                        logger.debug("\tclosed hand: %s" %
                                     self.table.players[0].closed_hand)
                        logger.debug("\topen hand: %s" %
                                     self.table.players[0].open_hand_34_tiles)
                        logger.debug("\tmeld tiles: %s" %
                                     self.table.players[0].meld_tiles)
                        # we had to add it to discards, to calculate remaining tiles correctly
                        # drawn tile is not the one drawn from rinshan, but
                        # the one previously discarded by player `p`
                        self.table.add_discarded_tile(0, discarded_tile, True,
                                                      previous_drawn_tile)
                        # after program discarding a card, next player is 1
                        p = 1
                        continue
                    else:  # ryuukyoku
                        logger.debug("Rinshan empty. Ryuukyoku!")
                        break
                elif meld:  # pon, chi
                    logger.debug("player 0 %s from %d: %s" %
                                 (meld.type, p, meld))
                    player_seat = 0
                    # DEBUG: we change the add_called_meld method, delete the
                    # part that changes self.table.count_of_remaining_tiles
                    self.table.add_called_meld(player_seat, meld)
                    # Equivalently, program draws the tile discarded by opponent
                    self.table.players[0].draw_tile(tile)
                    # check p win
                    if self.check_win(0):
                        logger.info("player {} wins (by {})!".format(
                            0, meld.type))
                        break
                    # if not win, we do the followings
                    if not self.table.players[0].in_riichi:
                        # choose a discard tile for playe `p`
                        discarded_tile = self.discard_tile(0)
                        # see if we can call riichi
                        if self.table.players[0].can_call_riichi():
                            logger.debug("player {} call riichi.".format(0))
                            self.table.players[0].in_riichi = True
                    else:  # if riichi, we can not call meld
                        raise ("Riichi player can not call meld!")
                    # remove the tile from player's hand
                    self.table.players[0].tiles.remove(discarded_tile)
                    # discarded tile added to table
                    self.table.add_discarded_tile(0, discarded_tile, True,
                                                  previous_drawn_tile)
                    logger.debug("player {} discards {} after {}".format(
                        0, discarded_tile, meld.type))
                    logger.debug("\tclosed hand: %s" %
                                 self.table.players[0].closed_hand)
                    logger.debug("\topen hand: %s" %
                                 self.table.players[0].open_hand_34_tiles)
                    logger.debug("\tmeld tiles: %s" %
                                 self.table.players[0].meld_tiles)
                    # after program discarding a card, next player is 1
                    p = 1
                    continue
            # we had to add it to discards, to calculate remaining tiles correctly
            self.table.add_discarded_tile(p, discarded_tile_tmp, is_tsumogiri,
                                          drawn_tile_tmp)

            # next player
            p = (p + 1) % 4

            logger.debug("[after]:%s,%s" %
                         (self.table.count_of_remaining_tiles,
                          len(self.table.remaining_tiles)))

        # output results
        logger.debug('\n')
        for p in range(4):
            logger.info("\tPlayer %d: %s (%s)" %
                        (p,
                         TilesConverter.to_one_line_string(
                             self.table.players[p].tiles),
                         TilesConverter.to_one_line_string(
                             self.table.players[p].closed_hand)))

        return bw_riichi, bw_stealing, r

    def get_WPET(self, Nsim):
        """Waiting probability at Each Turn
        """
        R = [0] * 19
        BW_riichi = [0] * 19
        BW_stealing = [0] * 19
        n = 0
        while n < Nsim:
            bw_riichi, bw_stealing, r = self.sim_WPET()
            print("%d. bw_riichi=%s, bw_stealing=%s, r=%s" %
                  (n + 1, bw_riichi, bw_stealing, r))
            for m in range(r + 1):  # from 0 to r
                R[m] += 1
            BW_riichi[r] += bw_riichi
            BW_stealing[r] += bw_stealing
            n += 1
        Wpet_riichi = [
            BW_riichi[n] * 1.0 / R[n] for n in range(len(BW_riichi))
        ]
        Wpet_stealing = [
            BW_stealing[n] * 1.0 / R[n] for n in range(len(BW_stealing))
        ]
        return BW_riichi, BW_stealing, R, Wpet_riichi, Wpet_stealing

    def sim_game(self):
        """Simulation of a riichi mahjong game, and record the bw and r parameters
        """
        # We don't need to fix the seed now.
        #seed = random.randint(1,100000)
        #seed = 73823
        #random.seed(seed)

        # start a new game
        self.initialize()
        # rinshan
        rinshan = self.table.dead_wall[0:4]
        # start from dealer
        p = self.table.dealer_seat
        #logger.info("seed: %d"%seed)
        logger.info("dealer seat: %d" % p)
        # Fixed: self.table.count_of_remaining_tiles>0 may not be accurate here
        while self.table.count_of_remaining_tiles > 0:  #len(self.table.remaining_tiles)>0:

            logger.debug("[begin]:%s,%s" %
                         (self.table.count_of_remaining_tiles,
                          len(self.table.remaining_tiles)))

            # update turn number
            self.table.turn_number[p] += 1

            # p draw a tile
            drawn_tile = self.table.remaining_tiles[0]
            self.table.players[p].draw_tile(drawn_tile)
            # check p win
            if self.check_win(p):
                logger.info("player {} wins (tsumo)!".format(p))
                tiles = self.table.players[p].tiles
                win_tile = drawn_tile
                (is_tsumo, is_riichi, is_dealer, open_sets, dora_indicators,
                 player_wind, round_wind) = self.check_status(p)
                ## TODO: Wait to be finished!
                result = self.finished_hand.estimate_hand_value(
                    tiles,
                    win_tile,
                    is_tsumo=is_tsumo,
                    is_riichi=is_riichi,
                    is_dealer=is_dealer,
                    is_ippatsu=False,
                    is_rinshan=False,
                    is_chankan=False,
                    is_haitei=False,
                    is_houtei=False,
                    is_daburu_riichi=False,
                    is_nagashi_mangan=False,
                    is_tenhou=False,
                    is_renhou=False,
                    is_chiihou=False,
                    open_sets=open_sets,
                    dora_indicators=dora_indicators,
                    called_kan_indices=None,
                    player_wind=player_wind,
                    round_wind=round_wind)
                logger.info(result)

                melds = self.table.players[p].melds
                result = self.hand_calculator.estimate_hand_value(
                    tiles,
                    win_tile,
                    melds=melds,
                    dora_indicators=dora_indicators,
                    config=HandConfig(is_tsumo=is_tsumo,
                                      is_riichi=is_riichi,
                                      player_wind=player_wind,
                                      round_wind=round_wind))
                logger.info(result)
                break

            # TODO: we only apply discard_tile strategy to our program, for the
            # opponent players, we assume they simply discard what they draw
            # if not win, we do the followings
            if True:  #p==0:
                if not self.table.players[p].in_riichi:
                    # choose a discard tile for playe `p`
                    discarded_tile = self.discard_tile(p)
                    # see if we can call riichi
                    if self.table.players[p].can_call_riichi():
                        logger.debug("player {} call riichi.".format(p))
                        self.table.players[p].in_riichi = True
                else:  # if riichi, we have to discard whatever we draw
                    discarded_tile = drawn_tile
            else:
                discarded_tile = drawn_tile

            # remove the tile from player's hand
            discarded_tile_tmp = discarded_tile
            drawn_tile_tmp = drawn_tile
            is_tsumogiri = (discarded_tile == drawn_tile)
            self.table.players[p].tiles.remove(discarded_tile)
            logger.debug("player {} discards {}".format(p, discarded_tile))
            logger.debug("\tclosed hand: %s" %
                         self.table.players[0].closed_hand)
            logger.debug("\topen hand: %s" %
                         self.table.players[0].open_hand_34_tiles)
            logger.debug("\tmeld tiles: %s" % self.table.players[0].meld_tiles)
            # now we check call meld
            # TODO: Ok, here we only allow our program to call meld. But maybe
            # we should allow the opponents to call meld too?
            if p != 0:
                previous_drawn_tile = drawn_tile
                tile = discarded_tile
                is_kamicha_discard = (p == 3)
                meld, discard_option = self.table.players[0].try_to_call_meld(
                    tile, is_kamicha_discard)
                kan_type = self.table.players[0].can_call_kan(tile, True)
                if kan_type:  # kan or chankan
                    tiles = [(tile // 4) * 4, (tile // 4) * 4 + 1,
                             (tile // 4) * 4 + 2, (tile // 4) * 4 + 3]
                    meld = self.call_meld(kan_type, 0, p, True, tiles, tile)
                    logger.debug("player 0 call kan from %d: %s" % (p, meld))
                    player_seat = meld.who
                    self.table.add_called_meld(player_seat, meld)
                    # we had to delete called tile from hand
                    # to have correct tiles count in the hand
                    # TODO[joseph]: I still don't know why we need this
                    # if meld type is: chi, pon, or nuki
                    # maybe no use here, b/c the meld type is always Meld.KAN here
                    if meld.type != Meld.KAN and meld.type != Meld.CHANKAN:
                        self.table.players[player_seat].draw_tile(
                            meld.called_tile)
                    # draw a tile from dead wall
                    if len(rinshan) > 0:
                        drawn_tile = rinshan.pop(0)
                        self.table.players[0].draw_tile(drawn_tile)
                        # check p win
                        if self.check_win(0):
                            logger.info(
                                "player {} wins (rinshan tsumo)!".format(0))
                            tiles = self.table.players[p].tiles
                            win_tile = drawn_tile
                            (is_tsumo, is_riichi, is_dealer, open_sets,
                             dora_indicators, player_wind,
                             round_wind) = self.check_status(p)
                            is_rinshan = True
                            ## TODO: Wait to be finished!
                            result = self.finished_hand.estimate_hand_value(
                                tiles,
                                win_tile,
                                is_tsumo=is_tsumo,
                                is_riichi=is_riichi,
                                is_dealer=is_dealer,
                                is_ippatsu=False,
                                is_rinshan=is_rinshan,
                                is_chankan=False,
                                is_haitei=False,
                                is_houtei=False,
                                is_daburu_riichi=False,
                                is_nagashi_mangan=False,
                                is_tenhou=False,
                                is_renhou=False,
                                is_chiihou=False,
                                open_sets=open_sets,
                                dora_indicators=dora_indicators,
                                called_kan_indices=None,
                                player_wind=player_wind,
                                round_wind=round_wind)
                            logger.info(result)

                            melds = self.table.players[p].melds
                            result = self.hand_calculator.estimate_hand_value(
                                tiles,
                                win_tile,
                                melds=melds,
                                dora_indicators=dora_indicators,
                                config=HandConfig(is_tsumo=is_tsumo,
                                                  is_riichi=is_riichi,
                                                  player_wind=player_wind,
                                                  round_wind=round_wind,
                                                  is_rinshan=is_rinshan))
                            logger.info(result)
                            break
                        # if not win, we do the followings
                        if not self.table.players[0].in_riichi:
                            # choose a discard tile for playe `p`
                            discarded_tile = self.discard_tile(0)
                            # see if we can call riichi
                            if self.table.players[0].can_call_riichi():
                                logger.info("player {} call riichi.".format(0))
                                self.table.players[0].in_riichi = True
                        else:  # if riichi, we have to discard whatever we draw
                            discarded_tile = drawn_tile
                        # remove the tile from player's hand
                        self.table.players[0].tiles.remove(discarded_tile)
                        logger.debug("player {} discards {} after kan".format(
                            0, discarded_tile))
                        logger.debug("\tclosed hand: %s" %
                                     self.table.players[0].closed_hand)
                        logger.debug("\topen hand: %s" %
                                     self.table.players[0].open_hand_34_tiles)
                        logger.debug("\tmeld tiles: %s" %
                                     self.table.players[0].meld_tiles)
                        # we had to add it to discards, to calculate remaining tiles correctly
                        # drawn tile is not the one drawn from rinshan, but
                        # the one previously discarded by player `p`
                        self.table.add_discarded_tile(0, discarded_tile, True,
                                                      previous_drawn_tile)
                        # after program discarding a card, next player is 1
                        p = 1
                        continue
                    else:  # ryuukyoku
                        logger.debug("Rinshan empty. Ryuukyoku!")
                        break
                elif meld:  # pon, chi
                    logger.debug("player 0 %s from %d: %s" %
                                 (meld.type, p, meld))
                    player_seat = 0
                    # DEBUG: we change the add_called_meld method, delete the
                    # part that changes self.table.count_of_remaining_tiles
                    self.table.add_called_meld(player_seat, meld)
                    # Equivalently, program draws the tile discarded by opponent
                    self.table.players[0].draw_tile(tile)
                    # check p win
                    if self.check_win(0):
                        logger.info("player {} wins (by {})!".format(
                            0, meld.type))
                        tiles = self.table.players[p].tiles
                        win_tile = tile
                        (is_tsumo, is_riichi, is_dealer, open_sets,
                         dora_indicators, player_wind,
                         round_wind) = self.check_status(p)
                        ## TODO: Wait to be finished!
                        result = self.finished_hand.estimate_hand_value(
                            tiles,
                            win_tile,
                            is_tsumo=is_tsumo,
                            is_riichi=is_riichi,
                            is_dealer=is_dealer,
                            is_ippatsu=False,
                            is_rinshan=False,
                            is_chankan=False,
                            is_haitei=False,
                            is_houtei=False,
                            is_daburu_riichi=False,
                            is_nagashi_mangan=False,
                            is_tenhou=False,
                            is_renhou=False,
                            is_chiihou=False,
                            open_sets=open_sets,
                            dora_indicators=dora_indicators,
                            called_kan_indices=None,
                            player_wind=player_wind,
                            round_wind=round_wind)
                        logger.info(result)

                        melds = self.table.players[p].melds
                        result = self.hand_calculator.estimate_hand_value(
                            tiles,
                            win_tile,
                            melds=melds,
                            dora_indicators=dora_indicators,
                            config=HandConfig(is_tsumo=is_tsumo,
                                              is_riichi=is_riichi,
                                              player_wind=player_wind,
                                              round_wind=round_wind))
                        logger.info(result)
                        break
                    # if not win, we do the followings
                    if not self.table.players[0].in_riichi:
                        # choose a discard tile for playe `p`
                        discarded_tile = self.discard_tile(0)
                        # see if we can call riichi
                        if self.table.players[0].can_call_riichi():
                            logger.debug("player {} call riichi.".format(0))
                            self.table.players[0].in_riichi = True
                    else:  # if riichi, we can not call meld
                        raise ("Riichi player can not call meld!")
                    # remove the tile from player's hand
                    self.table.players[0].tiles.remove(discarded_tile)
                    # discarded tile added to table
                    self.table.add_discarded_tile(0, discarded_tile, True,
                                                  previous_drawn_tile)
                    logger.debug("player {} discards {} after {}".format(
                        0, discarded_tile, meld.type))
                    logger.debug("\tclosed hand: %s" %
                                 self.table.players[0].closed_hand)
                    logger.debug("\topen hand: %s" %
                                 self.table.players[0].open_hand_34_tiles)
                    logger.debug("\tmeld tiles: %s" %
                                 self.table.players[0].meld_tiles)
                    # after program discarding a card, next player is 1
                    p = 1
                    continue
            # we had to add it to discards, to calculate remaining tiles correctly
            self.table.add_discarded_tile(p, discarded_tile_tmp, is_tsumogiri,
                                          drawn_tile_tmp)

            # next player
            p = (p + 1) % 4

            logger.debug("[after]:%s,%s" %
                         (self.table.count_of_remaining_tiles,
                          len(self.table.remaining_tiles)))

        # output results
        logger.debug('\n')
        for p in range(4):
            logger.info("\tPlayer %d: %s (%s)" %
                        (p,
                         TilesConverter.to_one_line_string(
                             self.table.players[p].tiles),
                         TilesConverter.to_one_line_string(
                             self.table.players[p].closed_hand)))

    ## TODO: finish this part!
    def check_status(self, p, is_tsumo=False):
        """ We want to check the status of player `p`
        :param is_tsumo:
        :param is_riichi:
        :param is_dealer:
        :param is_ippatsu:
        :param is_rinshan:
        :param is_chankan:
        :param is_haitei:
        :param is_houtei:
        :param is_tenhou:
        :param is_renhou:
        :param is_chiihou:
        :param is_daburu_riichi:
        :param is_nagashi_mangan:
        :param open_sets: array of array with open sets in 34-tile format
        :param dora_indicators: array of tiles in 136-tile format
        :param called_kan_indices: array of tiles in 136-tile format
        :param player_wind: index of player wind
        :param round_wind: index of round wind
        """
        is_tsumo = is_tsumo
        is_riichi = self.table.players[p].in_riichi
        is_dealer = (self.table.dealer_seat == p)
        open_sets = [
            meld.tiles for meld in self.table.players[p].melds
            if meld.opened == True
        ]
        open_sets = list(map(lambda lst: [l // 4 for l in lst],
                             open_sets))  # convert to 34 format
        dora_indicators = self.table.dora_indicators
        player_wind = self.table.players[p].player_wind
        round_wind = self.table.round_wind

        return (is_tsumo, is_riichi, is_dealer, open_sets, dora_indicators,
                player_wind, round_wind)

    def _check2(self):
        self.sim_game()

    def _check(self):
        BW_riichi, BW_stealing, R, Wpet_riichi, Wpet_stealing = self.get_WPET(
            500)
        print("BW riichi: %s" % BW_riichi)
        print("BW stealing: %s" % BW_stealing)
        print("R: %s" % R)
        print("WPET riichi: %s" % Wpet_riichi)
        print("WPET stealing: %s" % Wpet_stealing)

        import matplotlib.pyplot as plt
        plt.plot(range(19), Wpet_riichi, '*-r', range(19), Wpet_stealing,
                 'o-b')
Ejemplo n.º 29
0
    def test_is_chuuren_poutou(self):
        hand = HandCalculator()

        tiles = self._string_to_34_array(man="11112345678999")
        self.assertTrue(
            self.config.chuuren_poutou.is_condition_met(self._hand(tiles)))

        tiles = self._string_to_34_array(pin="11122345678999")
        self.assertTrue(
            self.config.chuuren_poutou.is_condition_met(self._hand(tiles)))

        tiles = self._string_to_34_array(sou="11123345678999")
        self.assertTrue(
            self.config.chuuren_poutou.is_condition_met(self._hand(tiles)))

        tiles = self._string_to_34_array(sou="11123445678999")
        self.assertTrue(
            self.config.chuuren_poutou.is_condition_met(self._hand(tiles)))

        tiles = self._string_to_34_array(sou="11123455678999")
        self.assertTrue(
            self.config.chuuren_poutou.is_condition_met(self._hand(tiles)))

        tiles = self._string_to_34_array(sou="11123456678999")
        self.assertTrue(
            self.config.chuuren_poutou.is_condition_met(self._hand(tiles)))

        tiles = self._string_to_34_array(sou="11123456778999")
        self.assertTrue(
            self.config.chuuren_poutou.is_condition_met(self._hand(tiles)))

        tiles = self._string_to_34_array(sou="11123456788999")
        self.assertTrue(
            self.config.chuuren_poutou.is_condition_met(self._hand(tiles)))

        tiles = self._string_to_34_array(sou="11123456789999")
        self.assertTrue(
            self.config.chuuren_poutou.is_condition_met(self._hand(tiles)))

        tiles = self._string_to_136_array(man="11123456789999")
        win_tile = self._string_to_136_tile(man="1")

        result = hand.estimate_hand_value(tiles, win_tile)
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 13)
        self.assertEqual(result.fu, 40)
        self.assertEqual(len(result.yaku), 1)

        daburi = [
            ["11122345678999", "2"],
            ["11123456789999", "9"],
            ["11112345678999", "1"],
        ]
        for hand_tiles, win_tile in daburi:
            tiles = self._string_to_136_array(man=hand_tiles)
            win_tile = self._string_to_136_tile(man=win_tile)

            result = hand.estimate_hand_value(tiles, win_tile)
            self.assertEqual(result.error, None)
            self.assertEqual(result.han, 26)
            self.assertEqual(len(result.yaku), 1)

        tiles = self._string_to_136_array(pin="111234566789999")
        win_tile = self._string_to_136_tile(pin="3")
        melds = [self._make_meld(Meld.KAN, pin="9999", is_open=False)]

        result = hand.estimate_hand_value(tiles, win_tile, melds=melds)
        self.assertEqual(result.error, None)
        self.assertEqual(result.han, 6)
        self.assertEqual(result.fu, 70)
        self.assertEqual(len(result.yaku), 1)
Ejemplo n.º 30
0
class ImplementationAI(InterfaceAI):
    version = '0.4.0'

    agari = None
    shanten_calculator = None
    defence = None
    riichi = None
    hand_divider = None
    finished_hand = None

    shanten = 7
    ukeire = 0
    ukeire_second = 0
    in_defence = False
    waiting = None

    current_strategy = None
    last_discard_option = None

    hand_cache = {}

    gpparams = {}

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

        self.agari = Agari()
        self.shanten_calculator = Shanten()
        self.defence = DefenceHandler(player)
        self.riichi = Riichi(player)
        self.hand_divider = HandDivider()
        self.finished_hand = HandCalculator()
        self.hand_builder = HandBuilder(player, self)

        self.erase_state()

    def erase_state(self):
        self.shanten = 7
        self.ukeire = 0
        self.ukeire_second = 0
        self.in_defence = False
        self.waiting = None

        self.current_strategy = None
        self.last_discard_option = None

        self.hand_cache = {}

    def load_params(self, params):
        self.gpparams = params

    def init_hand(self):
        DecisionsLogger.debug(
            log.INIT_HAND,
            context=[
                'Round  wind: {}'.format(
                    DISPLAY_WINDS[self.table.round_wind_tile]),
                'Player wind: {}'.format(
                    DISPLAY_WINDS[self.player.player_wind]),
                'Hand: {}'.format(self.player.format_hand_for_print()),
            ])

        self.shanten, _ = self.hand_builder.calculate_shanten(
            TilesConverter.to_34_array(self.player.tiles))

    def draw_tile(self, tile_136):
        self.determine_strategy(self.player.tiles)

    def discard_tile(self, discard_tile, print_log=True):
        # 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.hand_builder.process_discard_option(
                self.last_discard_option, self.player.closed_hand, True)

        return self.hand_builder.discard_tile(self.player.tiles,
                                              self.player.closed_hand,
                                              self.player.melds, print_log)

    def try_to_call_meld(self, tile_136, is_kamicha_discard, remaining_tiles):
        tiles_136_previous = self.player.tiles[:]
        tiles_136 = tiles_136_previous + [tile_136]
        self.determine_strategy(tiles_136)

        if not self.current_strategy:
            return None, None

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

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

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

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

        return meld, discard_option

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

        old_strategy = self.current_strategy
        self.current_strategy = None

        # order is important, we add strategies with the highest priority first
        strategies = []

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

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

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

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

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

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

        return self.current_strategy and True or False

    def estimate_hand_value(self,
                            win_tile,
                            tiles=None,
                            call_riichi=False,
                            is_tsumo=False):
        """
        :param win_tile: 34 tile format
        :param tiles:
        :param call_riichi:
        :param is_tsumo
        :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 = copy.copy(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_tile,
            has_aka_dora=self.player.table.has_aka_dora,
            has_open_tanyao=self.player.table.has_open_tanyao,
            is_tsumo=is_tsumo,
        )

        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):
        return self.riichi.should_call_riichi()

    def should_call_kan(self, tile, open_kan, from_riichi=False):
        """
        Method will decide should we call a kan,
        or upgrade pon to kan
        :param tile: 136 tile format
        :param open_kan: boolean
        :param from_riichi: boolean
        :return: kan type
        """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return None

    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
            DecisionsLogger.debug(log.DEFENCE_ACTIVATE)

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

    def _calculate_shanten_for_kan(self, tiles, closed_hand_tiles, melds):
        previous_results, previous_shanten = self.hand_builder.find_discard_options(
            tiles, closed_hand_tiles, melds)

        previous_results = [
            x for x in previous_results if x.shanten == previous_shanten
        ]

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

        previous_waits_cnt = sorted(previous_results,
                                    key=lambda x: -x.ukeire)[0].ukeire

        return previous_shanten, previous_waits_cnt
Ejemplo n.º 31
0
from mahjong.hand_calculating.hand import HandCalculator
from mahjong.meld import Meld
from mahjong.hand_calculating.hand_config import HandConfig
from mahjong.shanten import Shanten
from mahjong.tile import TilesConverter

calculator = HandCalculator()


# useful helper
def print_hand_result(hand_result):
    print(hand_result.han, hand_result.fu)
    print(hand_result.cost['main'])
    print(hand_result.yaku)
    for fu_item in hand_result.fu_details:
        print(fu_item)
    print('')


####################################################################
# Tanyao hand by ron                                               #
####################################################################

# we had to use all 14 tiles in that array
tiles = TilesConverter.string_to_136_array(man='22444',
                                           pin='333567',
                                           sou='444')
win_tile = TilesConverter.string_to_136_array(sou='4')[0]

result = calculator.estimate_hand_value(tiles, win_tile)
print_hand_result(result)
Ejemplo n.º 32
0
class ImplementationAI(InterfaceAI):
    version = '0.4.0'

    agari = None
    shanten_calculator = None
    defence = None
    riichi = None
    hand_divider = None
    finished_hand = None

    shanten = 7
    ukeire = 0
    ukeire_second = 0
    in_defence = False
    waiting = None

    current_strategy = None
    last_discard_option = None

    hand_cache = {}

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

        self.agari = Agari()
        self.shanten_calculator = Shanten()
        self.defence = DefenceHandler(player)
        self.riichi = Riichi(player)
        self.hand_divider = HandDivider()
        self.finished_hand = HandCalculator()
        self.hand_builder = HandBuilder(player, self)

        self.erase_state()

    def erase_state(self):
        self.shanten = 7
        self.ukeire = 0
        self.ukeire_second = 0
        self.in_defence = False
        self.waiting = None

        self.current_strategy = None
        self.last_discard_option = None

        self.hand_cache = {}

    def init_hand(self):
        DecisionsLogger.debug(log.INIT_HAND, context=[
            'Round  wind: {}'.format(DISPLAY_WINDS[self.table.round_wind_tile]),
            'Player wind: {}'.format(DISPLAY_WINDS[self.player.player_wind]),
            'Hand: {}'.format(self.player.format_hand_for_print()),
        ])

        self.shanten, _ = self.hand_builder.calculate_shanten(TilesConverter.to_34_array(self.player.tiles))

    def draw_tile(self, tile_136):
        self.determine_strategy(self.player.tiles)

    def discard_tile(self, discard_tile, print_log=True):
        # 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.hand_builder.process_discard_option(self.last_discard_option, self.player.closed_hand, True)

        return self.hand_builder.discard_tile(
            self.player.tiles,
            self.player.closed_hand,
            self.player.melds,
            print_log
        )

    def try_to_call_meld(self, tile_136, is_kamicha_discard):
        tiles_136_previous = self.player.tiles[:]
        tiles_136 = tiles_136_previous + [tile_136]
        self.determine_strategy(tiles_136)

        if not self.current_strategy:
            return None, None

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

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

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

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

        return meld, discard_option

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

        old_strategy = self.current_strategy
        self.current_strategy = None

        # order is important, we add strategies with the highest priority first
        strategies = []

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

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

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

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

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

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

        return self.current_strategy and True or False

    def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False, is_tsumo=False):
        """
        :param win_tile: 34 tile format
        :param tiles:
        :param call_riichi:
        :param is_tsumo
        :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 = copy.copy(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_tile,
            has_aka_dora=self.player.table.has_aka_dora,
            has_open_tanyao=self.player.table.has_open_tanyao,
            is_tsumo=is_tsumo,
        )

        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):
        return self.riichi.should_call_riichi()

    def should_call_kan(self, tile, open_kan, from_riichi=False):
        """
        Method will decide should we call a kan,
        or upgrade pon to kan
        :param tile: 136 tile format
        :param open_kan: boolean
        :param from_riichi: boolean
        :return: kan type
        """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return None

    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
            DecisionsLogger.debug(log.DEFENCE_ACTIVATE)

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

    def _calculate_shanten_for_kan(self, tiles, closed_hand_tiles, melds):
        previous_results, previous_shanten = self.hand_builder.find_discard_options(
            tiles,
            closed_hand_tiles,
            melds
        )

        previous_results = [x for x in previous_results if x.shanten == previous_shanten]

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

        previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire

        return previous_shanten, previous_waits_cnt