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
Exemple #2
0
    def is_condition_met(self, hand, win_tile, melds, is_tsumo):
        """
        Three closed pon sets, the other sets need not to be closed
        :param hand: list of hand's sets
        :param win_tile: 136 tiles format
        :param melds: list Meld objects
        :param is_tsumo:
        :return: true|false
        """
        win_tile //= 4

        open_sets = [x.tiles_34 for x in melds if x.opened]

        chi_sets = [x for x in hand if (is_chi(x) and win_tile in x and x not in open_sets)]
        pon_sets = [x for x in hand if is_pon(x)]

        closed_pon_sets = []
        for item in pon_sets:
            if item in open_sets:
                continue

            # if we do the ron on syanpon wait our pon will be consider as open
            # and it is not 789999 set
            if win_tile in item and not is_tsumo and not len(chi_sets):
                continue

            closed_pon_sets.append(item)

        return len(closed_pon_sets) == 3
Exemple #3
0
    def is_sanshoku(self, hand):
        """
        The same chi in three suits
        :param hand: list of hand's sets
        :return: true|false
        """
        chi_sets = [i for i in hand if is_chi(i)]
        if len(chi_sets) < 3:
            return False

        sou_chi = []
        pin_chi = []
        man_chi = []
        for item in chi_sets:
            if is_sou(item[0]):
                sou_chi.append(item)
            elif is_pin(item[0]):
                pin_chi.append(item)
            elif is_man(item[0]):
                man_chi.append(item)

        for sou_item in sou_chi:
            for pin_item in pin_chi:
                for man_item in man_chi:
                    # cast tile indices to 0..8 representation
                    sou_item = [simplify(x) for x in sou_item]
                    pin_item = [simplify(x) for x in pin_item]
                    man_item = [simplify(x) for x in man_item]
                    if sou_item == pin_item == man_item:
                        return True
        return False
Exemple #4
0
    def is_junchan(self, hand):
        """
        Every set must have at least one terminal, and the pair must be of
        a terminal tile. Must contain at least one sequence (123 or 789).
        :param hand: list of hand's sets
        :return: true|false
        """
        def tile_in_indices(item_set, indices_array):
            for x in item_set:
                if x in indices_array:
                    return True
            return False

        terminal_sets = 0
        count_of_chi = 0
        for item in hand:
            if is_chi(item):
                count_of_chi += 1

            if tile_in_indices(item, TERMINAL_INDICES):
                terminal_sets += 1

        if count_of_chi == 0:
            return False

        return terminal_sets == 5
Exemple #5
0
    def is_condition_met(self, hand, *args):
        chi_sets = [i for i in hand if is_chi(i)]
        if len(chi_sets) < 3:
            return False

        sou_chi = []
        pin_chi = []
        man_chi = []
        for item in chi_sets:
            if is_sou(item[0]):
                sou_chi.append(item)
            elif is_pin(item[0]):
                pin_chi.append(item)
            elif is_man(item[0]):
                man_chi.append(item)

        for sou_item in sou_chi:
            for pin_item in pin_chi:
                for man_item in man_chi:
                    # cast tile indices to 0..8 representation
                    sou_item = [simplify(x) for x in sou_item]
                    pin_item = [simplify(x) for x in pin_item]
                    man_item = [simplify(x) for x in man_item]
                    if sou_item == pin_item == man_item:
                        return True
        return False
Exemple #6
0
    def is_condition_met(self, hand, *args):
        chi_sets = [i for i in hand if is_chi(i)]
        if len(chi_sets) < 3:
            return False

        sou_chi = []
        pin_chi = []
        man_chi = []
        for item in chi_sets:
            if is_sou(item[0]):
                sou_chi.append(item)
            elif is_pin(item[0]):
                pin_chi.append(item)
            elif is_man(item[0]):
                man_chi.append(item)

        sets = [sou_chi, pin_chi, man_chi]

        for suit_item in sets:
            if len(suit_item) < 3:
                continue

            casted_sets = []

            for set_item in suit_item:
                # cast tiles indices to 0..8 representation
                casted_sets.append([simplify(set_item[0]),
                                    simplify(set_item[1]),
                                    simplify(set_item[2])])

            if [0, 1, 2] in casted_sets and [3, 4, 5] in casted_sets and [6, 7, 8] in casted_sets:
                return True

        return False
Exemple #7
0
        def is_valid_combination(possible_set):
            if is_chi(possible_set):
                return True

            if is_pon(possible_set):
                return True

            return False
    def is_condition_met(self, hand, *args):
        chi_sets = [i for i in hand if is_chi(i)]
        count_of_identical_chi = []
        for x in chi_sets:
            count = 0
            for y in chi_sets:
                if x == y:
                    count += 1
            count_of_identical_chi.append(count)

        return len([x for x in count_of_identical_chi if x >= 2]) == 4
Exemple #9
0
    def is_condition_met(self, hand, *args):
        chi_sets = [i for i in hand if is_chi(i)]

        count_of_identical_chi = 0
        for x in chi_sets:
            count = 0
            for y in chi_sets:
                if x == y:
                    count += 1
            if count > count_of_identical_chi:
                count_of_identical_chi = count

        return count_of_identical_chi >= 2
Exemple #10
0
    def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand,
                                discarded_tile):
        discarded_tile_34 = discarded_tile // 4

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

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

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

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

            tiles = [first_tile, second_tile, discarded_tile]

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

            melds = self.player.melds + [meld]

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

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

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

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

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

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

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

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

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

            tiles = [
                first_tile,
                second_tile,
                discarded_tile
            ]

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

            melds = self.player.melds + [meld]

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

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

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

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

        return final_results[0]
Exemple #12
0
    def is_ryanpeiko(self, hand):
        """
        The hand contains two different Iipeikou’s
        :param hand: list of hand's sets
        :return: true|false
        """
        chi_sets = [i for i in hand if is_chi(i)]
        count_of_identical_chi = []
        for x in chi_sets:
            count = 0
            for y in chi_sets:
                if x == y:
                    count += 1
            count_of_identical_chi.append(count)

        return len([x for x in count_of_identical_chi if x >= 2]) == 4
Exemple #13
0
    def is_iipeiko(self, hand):
        """
        Hand with two identical chi
        :param hand: list of hand's sets
        :return: true|false
        """
        chi_sets = [i for i in hand if is_chi(i)]

        count_of_identical_chi = 0
        for x in chi_sets:
            count = 0
            for y in chi_sets:
                if x == y:
                    count += 1
            if count > count_of_identical_chi:
                count_of_identical_chi = count

        return count_of_identical_chi >= 2
Exemple #14
0
    def is_ittsu(self, hand):
        """
        Three sets of same suit: 1-2-3, 4-5-6, 7-8-9
        :param hand: list of hand's sets
        :return: true|false
        """

        chi_sets = [i for i in hand if is_chi(i)]
        if len(chi_sets) < 3:
            return False

        sou_chi = []
        pin_chi = []
        man_chi = []
        for item in chi_sets:
            if is_sou(item[0]):
                sou_chi.append(item)
            elif is_pin(item[0]):
                pin_chi.append(item)
            elif is_man(item[0]):
                man_chi.append(item)

        sets = [sou_chi, pin_chi, man_chi]

        for suit_item in sets:
            if len(suit_item) < 3:
                continue

            casted_sets = []

            for set_item in suit_item:
                # cast tiles indices to 0..8 representation
                casted_sets.append([
                    simplify(set_item[0]),
                    simplify(set_item[1]),
                    simplify(set_item[2])
                ])

            if [0, 1, 2] in casted_sets and [3, 4, 5] in casted_sets and [
                    6, 7, 8
            ] in casted_sets:
                return True

        return False
Exemple #15
0
    def is_condition_met(self, hand, *args):
        def tile_in_indices(item_set, indices_array):
            for x in item_set:
                if x in indices_array:
                    return True
            return False

        terminal_sets = 0
        count_of_chi = 0
        for item in hand:
            if is_chi(item):
                count_of_chi += 1

            if tile_in_indices(item, TERMINAL_INDICES):
                terminal_sets += 1

        if count_of_chi == 0:
            return False

        return terminal_sets == 5
    def _should_call_riichi_one_sided(self, waiting):
        count_tiles = self.player.ai.hand_builder.count_tiles(
            waiting, TilesConverter.to_34_array(self.player.closed_hand))
        waiting = waiting[0]
        hand_value = self.player.ai.estimate_hand_value_or_get_from_cache(
            waiting, call_riichi=False)
        hand_value_with_riichi = self.player.ai.estimate_hand_value_or_get_from_cache(
            waiting, call_riichi=True)

        must_riichi = self.player.ai.placement.must_riichi(
            has_yaku=(hand_value.yaku is not None
                      and hand_value.cost is not None),
            num_waits=count_tiles,
            cost_with_riichi=hand_value_with_riichi.cost["main"],
            cost_with_damaten=(hand_value.cost and hand_value.cost["main"]
                               or 0),
        )
        if must_riichi == Placement.MUST_RIICHI:
            return True
        elif must_riichi == Placement.MUST_DAMATEN:
            return False

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

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

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

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

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

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

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

                return False

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

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

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

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

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

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

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

                    return True

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

                    # early riichi on 19 tanki is good
                    if (simplified_waiting == 0 or simplified_waiting
                            == 8) and self.player.round_step < 7:
                        return True

                    # riichi on 19 tanki is good later too if we have 3 tiles to wait for
                    if ((simplified_waiting == 0 or simplified_waiting == 8)
                            and self.player.round_step < 12
                            and count_tiles == 3):
                        return True

                    # riichi on 28 tanki is good if we have 3 tiles to wait for
                    if ((simplified_waiting == 1 or simplified_waiting == 7)
                            and self.player.round_step < 12
                            and count_tiles == 3):
                        return True

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

                    return True

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

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

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

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

                    # if we have more than two tiles to wait for and we have kabe or suji - insta riichi
                    if count_tiles > 2 and (have_suji or have_kabe):
                        return True

                    # 2 and 8 are good waits but not in every condition
                    if simplified_waiting == 1 or simplified_waiting == 7:
                        if self.player.round_step < 7:
                            if self.player.is_dealer and min_cost < 18000:
                                return True

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

                        if self.player.round_step < 12:
                            if self.player.is_dealer and min_cost < 12000:
                                return True

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

                        if self.player.round_step < 15:
                            if self.player.is_dealer and 2000 < min_cost < 7700:
                                return True

                    # 3 and 7 are ok waits sometimes too
                    if simplified_waiting == 2 or simplified_waiting == 6:
                        if self.player.round_step < 7:
                            if self.player.is_dealer and min_cost < 12000:
                                return True

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

                        if self.player.round_step < 12:
                            if self.player.is_dealer and min_cost < 7700:
                                return True

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

                        if self.player.round_step < 15:
                            if self.player.is_dealer and 2000 < min_cost < 7700:
                                return True

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

                    return True

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

        if count_tiles > 1:
            # terminal tanki is ok, too, just should be more than one tile left
            if is_terminal(waiting):
                return True

            # whatever dora wait is ok, too, just should be more than one tile left
            if plus_dora(waiting * 4,
                         self.player.table.dora_indicators,
                         add_aka_dora=False) > 0:
                return True

        simplified_waiting = simplify(waiting)

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

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

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

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

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

        return True
Exemple #17
0
def boxes_is_chi(boxes):
    items = sorted([t[0] for t in boxes[0:3]])
    if is_honor(items[0]) or is_honor(items[2]):
        return False
    return is_chi(items)
Exemple #18
0
    def _should_call_riichi_one_sided(self):
        count_tiles = self.player.ai.hand_builder.count_tiles(
            self.player.ai.waiting, TilesConverter.to_34_array(self.player.closed_hand)
        )
        waiting = self.player.ai.waiting[0]
        hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False)

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

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

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

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

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

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

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

                return False

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

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

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

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

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

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

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

                    return True

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

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

                    return True

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

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

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

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

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

                    return True

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

        simplified_waiting = simplify(waiting)

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

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

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

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

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

        return True
Exemple #19
0
    def estimate_hand_value(self,
                            tiles,
                            win_tile,
                            melds=None,
                            dora_indicators=None,
                            config=None):
        """
        :param tiles: array with 14 tiles in 136-tile format
        :param win_tile: 136 format tile that caused win (ron or tsumo)
        :param melds: array with Meld objects
        :param dora_indicators: array of tiles in 136-tile format
        :param config: HandConfig object
        :return: HandResponse object
        """

        if not melds:
            melds = []

        if not dora_indicators:
            dora_indicators = []

        self.config = config or HandConfig()

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

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

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

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

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

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

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

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

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

        hand_options = divider.divide_hand(tiles_34, melds)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                    count_of_dora = 0
                    count_of_aka_dora = 0

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

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

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

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

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

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

                calculated_hands.append(calculated_hand)

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

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

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

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

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

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

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

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

        return HandResponse(cost, han, fu, hand_yaku, error, fu_details)
Exemple #20
0
    def calculate_additional_fu(self, win_tile, hand, is_tsumo, player_wind,
                                round_wind, open_sets, called_kan_indices):
        """
        :param win_tile: "136 format" tile
        :param hand: list of hand's sets
        :param player_wind:
        :param round_wind:
        :param open_sets: array of array with 34 tiles format
        :param called_kan_indices: array of 34 tiles format
        :return: int
        """
        win_tile //= 4
        additional_fu = 0

        closed_hand = []
        for set_item in hand:
            if not is_pair(set_item) and set_item not in open_sets:
                closed_hand.append(set_item)

        pon_sets = [x for x in hand if is_pon(x)]
        chi_sets = [x for x in hand if (win_tile in x and is_chi(x))]
        closed_hand_indices = closed_hand and reduce(lambda z, y: z + y,
                                                     closed_hand) or []

        # there is no sense to check identical sets
        unique_chi_sets = []
        for item in chi_sets:
            if item not in unique_chi_sets:
                unique_chi_sets.append(item)

        chi_fu_sets = []
        for set_item in unique_chi_sets:
            count_of_open_sets = len([x for x in open_sets if x == set_item])
            count_of_sets = len([x for x in chi_sets if x == set_item])
            if count_of_open_sets == count_of_sets:
                continue

            # penchan waiting
            if any(x in set_item for x in TERMINAL_INDICES):
                tile_number = simplify(win_tile)
                # 1-2-...
                if set_item.index(win_tile) == 2 and tile_number == 2:
                    chi_fu_sets.append(set_item)
                # ...-8-9
                elif set_item.index(win_tile) == 0 and tile_number == 6:
                    chi_fu_sets.append(set_item)

            # kanchan waiting 5-...-7
            if set_item.index(win_tile) == 1:
                chi_fu_sets.append(set_item)

        for set_item in pon_sets:
            set_was_open = set_item in open_sets
            is_kan = set_item[0] in called_kan_indices
            is_honor = set_item[0] in TERMINAL_INDICES + HONOR_INDICES

            # we win on the third pon tile, our pon will be count as open
            if not is_tsumo and win_tile in set_item:
                # 111123 form is exception
                if len([x for x in closed_hand_indices if x == win_tile]) != 4:
                    set_was_open = True

            if is_honor:
                if is_kan:
                    additional_fu += set_was_open and 16 or 32
                else:
                    additional_fu += set_was_open and 4 or 8
            else:
                if is_kan:
                    additional_fu += set_was_open and 8 or 16
                else:
                    additional_fu += set_was_open and 2 or 4

        # valued pair
        pair = [x for x in hand if is_pair(x)][0][0]
        valued_indices = [HAKU, HATSU, CHUN, player_wind, round_wind]
        count_of_valued_pairs = [x for x in valued_indices if x == pair]
        if len(count_of_valued_pairs):
            # we can have 4 fu for east-east pair
            additional_fu += 2 * len(count_of_valued_pairs)

        pair_was_counted = False
        if len(chi_fu_sets) and len(unique_chi_sets) == len(chi_fu_sets):
            if len(chi_fu_sets
                   ) == 1 and pair in chi_fu_sets[0] and win_tile == pair:
                additional_fu += 2
                pair_was_counted = True
            else:
                additional_fu += 2
                pair_was_counted = True
        elif additional_fu != 0 and len(chi_fu_sets):
            # Hand like 123345
            # we can't count pinfu yaku here, so let's add additional fu for 123 waiting
            pair_was_counted = True
            additional_fu += 2

        # separate pair waiting
        if pair == win_tile:
            if not len(chi_sets):
                additional_fu += 2
            elif additional_fu != 0 and not pair_was_counted:
                # we can't count pinfu yaku here, so let's add additional fu
                additional_fu += 2

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

        closed_hand = self.player.closed_hand[:]

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

        # we can't use this tile for our chosen strategy
        if not self.is_tile_suitable(tile):
            return None, None

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

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

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

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

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

        if combinations:
            combinations = combinations[0]

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

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

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

        if not possible_melds:
            return None, None

        chosen_meld_dict = self._find_best_meld_to_open(tile, possible_melds, new_tiles, closed_hand, tile)
        # we didn't find a good discard candidate after open meld
        if not chosen_meld_dict:
            return None, None

        selected_tile = chosen_meld_dict["discard_tile"]
        meld = chosen_meld_dict["meld"]

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

        # each strategy can use their own value to min shanten number
        if shanten > self.min_shanten:
            self.player.logger.debug(
                log.MELD_DEBUG,
                "After meld shanten is too high for our strategy. Abort melding.",
            )
            return None, None

        # sometimes we had to call tile, even if it will not improve our hand
        # otherwise we can call only with improvements of shanten
        if not had_to_be_called and shanten >= self.player.ai.shanten:
            self.player.logger.debug(
                log.MELD_DEBUG,
                "Meld is not improving hand shanten. Abort melding.",
            )
            return None, None

        if not self.validate_meld(chosen_meld_dict):
            self.player.logger.debug(
                log.MELD_DEBUG,
                "Meld is suitable for strategy logic. Abort melding.",
            )
            return None, None

        if not self.should_push_against_threats(chosen_meld_dict):
            self.player.logger.debug(
                log.MELD_DEBUG,
                "Meld is too dangerous to call. Abort melding.",
            )
            return None, None

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

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

        closed_hand = self.player.closed_hand[:]

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

        # we can't use this tile for our chosen strategy
        if not self.is_tile_suitable(tile):
            return None, None

        discarded_tile = tile // 4
        new_tiles = self.player.tiles[:] + [tile]
        closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile])

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

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

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

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

        if combinations:
            combinations = combinations[0]

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

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

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

        if not possible_melds:
            return None, None

        best_meld_34 = self._find_best_meld_to_open(possible_melds, new_tiles)
        if best_meld_34:
            # we need to calculate count of shanten with supposed meld
            # to prevent bad hand openings
            melds = self.player.open_hand_34_tiles + [best_meld_34]
            outs_results, shanten = self.player.ai.calculate_outs(new_tiles, closed_hand, melds)

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

            # we can't improve hand, so we don't need to open it
            if not outs_results:
                return None, None

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

            meld_type = is_chi(best_meld_34) and Meld.CHI or Meld.PON
            best_meld_34.remove(discarded_tile)

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

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

            tiles = [
                first_tile,
                second_tile,
                tile
            ]

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

            # we had to be sure that all our discard results exists in the closed hand
            filtered_results = []
            for result in outs_results:
                if result.find_tile_in_hand(closed_hand):
                    filtered_results.append(result)

            # we can't discard anything, so let's not open our hand
            if not filtered_results:
                return None, None

            selected_tile = self.player.ai.process_discard_options_and_select_tile_to_discard(
                filtered_results,
                shanten,
                had_was_open=True
            )

            return meld, selected_tile

        return None, None
Exemple #23
0
    def find_valid_combinations(self, tiles_34, first_index, second_index, hand_not_completed=False):
        """
        Find and return all valid set combinations in given suit
        :param tiles_34:
        :param first_index:
        :param second_index:
        :param hand_not_completed: in that mode we can return just possible shi or pon sets
        :return: list of valid combinations
        """
        indices = []
        for x in range(first_index, second_index + 1):
            if tiles_34[x] > 0:
                indices.extend([x] * tiles_34[x])

        if not indices:
            return []

        all_possible_combinations = list(itertools.permutations(indices, 3))

        def is_valid_combination(possible_set):
            if is_chi(possible_set):
                return True

            if is_pon(possible_set):
                return True

            return False

        valid_combinations = []
        for combination in all_possible_combinations:
            if is_valid_combination(combination):
                valid_combinations.append(list(combination))

        if not valid_combinations:
            return []

        count_of_needed_combinations = int(len(indices) / 3)

        # simple case, we have count of sets == count of tiles
        if (
            count_of_needed_combinations == len(valid_combinations)
            and reduce(lambda z, y: z + y, valid_combinations) == indices
        ):
            return [valid_combinations]

        # filter and remove not possible pon sets
        for item in valid_combinations:
            if is_pon(item):
                count_of_sets = 1
                count_of_tiles = 0
                while count_of_sets > count_of_tiles:
                    count_of_tiles = len([x for x in indices if x == item[0]]) / 3
                    count_of_sets = len(
                        [x for x in valid_combinations if x[0] == item[0] and x[1] == item[1] and x[2] == item[2]]
                    )

                    if count_of_sets > count_of_tiles:
                        valid_combinations.remove(item)

        # filter and remove not possible chi sets
        for item in valid_combinations:
            if is_chi(item):
                count_of_sets = 5
                # TODO calculate real count of possible sets
                count_of_possible_sets = 4
                while count_of_sets > count_of_possible_sets:
                    count_of_sets = len(
                        [x for x in valid_combinations if x[0] == item[0] and x[1] == item[1] and x[2] == item[2]]
                    )

                    if count_of_sets > count_of_possible_sets:
                        valid_combinations.remove(item)

        # lit of chi\pon sets for not completed hand
        if hand_not_completed:
            return [valid_combinations]

        # hard case - we can build a lot of sets from our tiles
        # for example we have 123456 tiles and we can build sets:
        # [1, 2, 3] [4, 5, 6] [2, 3, 4] [3, 4, 5]
        # and only two of them valid in the same time [1, 2, 3] [4, 5, 6]

        possible_combinations = set(
            itertools.permutations(range(0, len(valid_combinations)), count_of_needed_combinations)
        )

        combinations_results = []
        for combination in possible_combinations:
            result = []
            for item in combination:
                result += valid_combinations[item]
            result = sorted(result)

            if result == indices:
                results = []
                for item in combination:
                    results.append(valid_combinations[item])
                results = sorted(results, key=lambda z: z[0])
                if results not in combinations_results:
                    combinations_results.append(results)

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

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

        closed_hand = self.player.closed_hand[:]

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

        # we can't use this tile for our chosen strategy
        if not self.is_tile_suitable(tile):
            return None, None

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

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

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

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

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

        if combinations:
            combinations = combinations[0]

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

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

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

        if not possible_melds:
            return None, None

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

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

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

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

        return meld, selected_tile
    def _find_best_meld_to_open(self, call_tile_136, possible_melds, new_tiles, closed_hand, discarded_tile):
        all_tiles_are_suitable = True
        for tile_136 in closed_hand:
            all_tiles_are_suitable &= self.is_tile_suitable(tile_136)

        final_results = []
        for meld_34 in possible_melds:
            # in order to fully emulate the possible hand with meld, we save original melds state,
            # modify player's melds and then restore original melds state after everything is done
            melds_original = self.player.melds[:]
            tiles_original = self.player.tiles[:]

            tiles = self._find_meld_tiles(closed_hand, meld_34, discarded_tile)
            meld = MeldPrint()
            meld.type = is_chi(meld_34) and MeldPrint.CHI or MeldPrint.PON
            meld.tiles = sorted(tiles)

            self.player.logger.debug(
                log.MELD_HAND, f"Hand: {self._format_hand_for_print(closed_hand, discarded_tile, self.player.melds)}"
            )

            # update player hand state to emulate new situation and choose what to discard
            self.player.tiles = new_tiles[:]
            self.player.add_called_meld(meld)

            selected_tile = self.player.ai.hand_builder.choose_tile_to_discard(after_meld=True)

            # restore original tiles and melds state
            self.player.tiles = tiles_original
            self.player.melds = melds_original

            # we can't find a good discard candidate, so let's skip this
            if not selected_tile:
                self.player.logger.debug(log.MELD_DEBUG, "Can't find discard candidate after meld. Abort melding.")
                continue

            if not all_tiles_are_suitable and self.is_tile_suitable(selected_tile.tile_to_discard_136):
                self.player.logger.debug(
                    log.MELD_DEBUG,
                    "We have tiles in our hand that are not suitable to current strategy, "
                    "but we are going to discard tile that we need. Abort melding.",
                )
                continue

            call_tile_34 = call_tile_136 // 4
            # we can't discard the same tile that we called
            if selected_tile.tile_to_discard_34 == call_tile_34:
                self.player.logger.debug(
                    log.MELD_DEBUG, "We can't discard same tile that we used for meld. Abort melding."
                )
                continue

            # we can't discard tile from the other end of the same ryanmen that we called
            if not is_honor(selected_tile.tile_to_discard_34) and meld.type == MeldPrint.CHI:
                if is_sou(selected_tile.tile_to_discard_34) and is_sou(call_tile_34):
                    same_suit = True
                elif is_man(selected_tile.tile_to_discard_34) and is_man(call_tile_34):
                    same_suit = True
                elif is_pin(selected_tile.tile_to_discard_34) and is_pin(call_tile_34):
                    same_suit = True
                else:
                    same_suit = False

                if same_suit:
                    simplified_meld_0 = simplify(meld.tiles[0] // 4)
                    simplified_meld_1 = simplify(meld.tiles[1] // 4)
                    simplified_call = simplify(call_tile_34)
                    simplified_discard = simplify(selected_tile.tile_to_discard_34)
                    kuikae = False
                    if simplified_discard == simplified_call - 3:
                        kuikae_set = [simplified_call - 1, simplified_call - 2]
                        if simplified_meld_0 in kuikae_set and simplified_meld_1 in kuikae_set:
                            kuikae = True
                    elif simplified_discard == simplified_call + 3:
                        kuikae_set = [simplified_call + 1, simplified_call + 2]
                        if simplified_meld_0 in kuikae_set and simplified_meld_1 in kuikae_set:
                            kuikae = True

                    if kuikae:
                        tile_str = TilesConverter.to_one_line_string(
                            [selected_tile.tile_to_discard_136], print_aka_dora=self.player.table.has_aka_dora
                        )
                        self.player.logger.debug(
                            log.MELD_DEBUG,
                            f"Kuikae discard {tile_str} candidate. Abort melding.",
                        )
                        continue

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

        if not final_results:
            self.player.logger.debug(log.MELD_DEBUG, "There are no good discards after melding.")
            return None

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

        self.player.logger.debug(
            log.MELD_PREPARE,
            "Tiles could be used for open meld",
            context=final_results,
        )
        return final_results[0]
Exemple #26
0
    def try_to_call_meld(self, tile, is_kamicha_discard):
        """
        When bot can open hand with a set (chi or pon/kan) this method will be called
        :param tile: 136 format tile
        :param is_kamicha_discard: boolean
        :return: Meld and DiscardOption objects or None, None
        """

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

        closed_hand = self.player.closed_hand[:]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            tiles_136 = [
                first_tile,
                second_tile,
                tile
            ]

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

        return None, None
Exemple #27
0
    def estimate_hand_value(
        self,
        tiles,
        win_tile,
        melds=None,
        dora_indicators=None,
        config=None,
        scores_calculator_factory=ScoresCalculator,
        use_hand_divider_cache=False,
    ):
        """
        :param tiles: array with 14 tiles in 136-tile format
        :param win_tile: 136 format tile that caused win (ron or tsumo)
        :param melds: array with Meld objects
        :param dora_indicators: array of tiles in 136-tile format
        :param config: HandConfig object
        :param use_hand_divider_cache: could be useful if you are calculating a lot of menchin hands
        :return: HandResponse object
        """

        if not melds:
            melds = []

        if not dora_indicators:
            dora_indicators = []

        self.config = config or HandConfig()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                    count_of_dora = 0
                    count_of_aka_dora = 0

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

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

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

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

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

                if fu == 0 and is_aotenjou:
                    fu = 40

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

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

                calculated_hands.append(calculated_hand)

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

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

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

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

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

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

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

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

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

                tiles_for_dora = tiles[:]

                count_of_dora = 0
                count_of_aka_dora = 0

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

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

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

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

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

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

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

        return HandResponse(cost, han, fu, hand_yaku, error, fu_details,
                            is_open_hand)
Exemple #28
0
    def estimate_hand_value(self,
                            tiles,
                            win_tile,
                            is_tsumo=False,
                            is_riichi=False,
                            is_dealer=False,
                            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=None,
                            dora_indicators=None,
                            called_kan_indices=None,
                            player_wind=None,
                            round_wind=None):
        """
        :param tiles: array with 14 tiles in 136-tile format
        :param win_tile: tile that caused win (ron or tsumo)
        :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 136-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
        :return: The dictionary with hand cost or error response

        {"cost": {'main': 1000, 'additional': 0}, "han": 1, "fu": 30, "error": None, "hand_yaku": []}
        {"cost": None, "han": 0, "fu": 0, "error": "Hand is not valid", "hand_yaku": []}
        """
        if not open_sets:
            open_sets = []
        else:
            # cast 136 format to 34 format
            for item in open_sets:
                item[0] //= 4
                item[1] //= 4
                item[2] //= 4
        is_open_hand = len(open_sets) > 0

        if not dora_indicators:
            dora_indicators = []

        kan_indices_136 = []
        if not called_kan_indices:
            called_kan_indices = []
        else:
            kan_indices_136 = called_kan_indices
            called_kan_indices = [x // 4 for x in called_kan_indices]

        agari = Agari()
        cost = None
        error = None
        hand_yaku = []
        han = 0
        fu = 0

        def return_response():
            return {
                'cost': cost,
                'error': error,
                'han': han,
                'fu': fu,
                'hand_yaku': hand_yaku
            }

        # special situation
        if is_nagashi_mangan:
            hand_yaku.append(yaku.nagashi_mangan)
            fu = 30
            han = yaku.nagashi_mangan.han['closed']
            cost = self.calculate_scores(han, fu, is_tsumo, is_dealer)
            return return_response()

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

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

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

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

        tiles_34 = TilesConverter.to_34_array(tiles)
        divider = HandDivider()

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

        hand_options = divider.divide_hand(tiles_34, open_sets,
                                           called_kan_indices)

        calculated_hands = []
        for hand in hand_options:
            cost = None
            error = None
            hand_yaku = []
            han = 0
            fu = 0

            if is_tsumo or is_open_hand:
                fu += 20
            else:
                fu += 30

            pon_sets = [x for x in hand if is_pon(x)]
            chi_sets = [x for x in hand if is_chi(x)]
            additional_fu = self.calculate_additional_fu(
                win_tile, hand, is_tsumo, player_wind, round_wind, open_sets,
                called_kan_indices)

            if additional_fu == 0 and len(chi_sets) == 4:
                """
                - A hand without pon and kan sets, so it should contains all sequences and a pair
                - The pair should be not valued
                - The waiting must be an open wait (on 2 different tiles)
                - Hand should be closed
                """
                if is_open_hand:
                    fu += 2
                    is_pinfu = False
                else:
                    is_pinfu = True
            else:
                fu += additional_fu
                is_pinfu = False

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

                # pinfu + tsumo always is 20 fu
                if not is_pinfu:
                    fu += 2

            if is_pinfu:
                hand_yaku.append(yaku.pinfu)

            is_chitoitsu = self.is_chitoitsu(hand)
            # let's skip hand that looks like chitoitsu, but it contains open sets
            if is_chitoitsu and is_open_hand:
                continue

            if is_chitoitsu:
                hand_yaku.append(yaku.chiitoitsu)

            is_tanyao = self.is_tanyao(hand)
            if is_open_hand and not settings.OPEN_TANYAO:
                is_tanyao = False

            if is_tanyao:
                hand_yaku.append(yaku.tanyao)

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

            if is_daburu_riichi:
                hand_yaku.append(yaku.daburu_riichi)

            if is_ippatsu:
                hand_yaku.append(yaku.ippatsu)

            if is_rinshan:
                hand_yaku.append(yaku.rinshan)

            if is_chankan:
                hand_yaku.append(yaku.chankan)

            if is_haitei:
                hand_yaku.append(yaku.haitei)

            if is_houtei:
                hand_yaku.append(yaku.houtei)

            if is_renhou:
                hand_yaku.append(yaku.renhou)

            if is_tenhou:
                hand_yaku.append(yaku.tenhou)

            if is_chiihou:
                hand_yaku.append(yaku.chiihou)

            if self.is_honitsu(hand):
                hand_yaku.append(yaku.honitsu)

            if self.is_chinitsu(hand):
                hand_yaku.append(yaku.chinitsu)

            if self.is_tsuisou(hand):
                hand_yaku.append(yaku.tsuisou)

            if self.is_honroto(hand):
                hand_yaku.append(yaku.honroto)

            if self.is_chinroto(hand):
                hand_yaku.append(yaku.chinroto)

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

                if self.is_junchan(hand):
                    hand_yaku.append(yaku.junchan)

                if self.is_ittsu(hand):
                    hand_yaku.append(yaku.ittsu)

                if not is_open_hand:
                    if self.is_ryanpeiko(hand):
                        hand_yaku.append(yaku.ryanpeiko)
                    elif self.is_iipeiko(hand):
                        hand_yaku.append(yaku.iipeiko)

                if self.is_sanshoku(hand):
                    hand_yaku.append(yaku.sanshoku)

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

                if self.is_sanankou(win_tile, hand, open_sets, is_tsumo):
                    hand_yaku.append(yaku.sanankou)

                if self.is_sanshoku_douko(hand):
                    hand_yaku.append(yaku.sanshoku_douko)

                if self.is_shosangen(hand):
                    hand_yaku.append(yaku.shosangen)

                if self.is_haku(hand):
                    hand_yaku.append(yaku.haku)

                if self.is_hatsu(hand):
                    hand_yaku.append(yaku.hatsu)

                if self.is_chun(hand):
                    hand_yaku.append(yaku.hatsu)

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

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

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

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

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

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

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

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

                if self.is_daisangen(hand):
                    hand_yaku.append(yaku.daisangen)

                if self.is_shosuushi(hand):
                    hand_yaku.append(yaku.shosuushi)

                if self.is_daisuushi(hand):
                    hand_yaku.append(yaku.daisuushi)

                if self.is_ryuisou(hand):
                    hand_yaku.append(yaku.ryuisou)

                if not is_open_hand and self.is_chuuren_poutou(hand):
                    if tiles_34[win_tile // 4] == 2:
                        hand_yaku.append(yaku.daburu_chuuren_poutou)
                    else:
                        hand_yaku.append(yaku.chuuren_poutou)

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

                if self.is_sankantsu(hand, called_kan_indices):
                    hand_yaku.append(yaku.sankantsu)

                if self.is_suukantsu(hand, called_kan_indices):
                    hand_yaku.append(yaku.suukantsu)

            # chitoitsu is always 25 fu
            if is_chitoitsu:
                fu = 25

            tiles_for_dora = tiles + kan_indices_136
            count_of_dora = 0
            count_of_aka_dora = 0
            for tile in tiles_for_dora:
                count_of_dora += plus_dora(tile, dora_indicators)

            for tile in tiles_for_dora:
                if is_aka_dora(tile):
                    count_of_aka_dora += 1

            if count_of_dora:
                yaku_item = yaku.dora
                yaku_item.han['open'] = count_of_dora
                yaku_item.han['closed'] = count_of_dora
                hand_yaku.append(yaku_item)

            if count_of_aka_dora:
                yaku_item = yaku.aka_dora
                yaku_item.han['open'] = count_of_aka_dora
                yaku_item.han['closed'] = count_of_aka_dora
                hand_yaku.append(yaku_item)

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

            # calculate han
            for item in hand_yaku:
                if is_open_hand and item.han['open']:
                    han += item.han['open']
                else:
                    han += item.han['closed']

            # round up
            # 22 -> 30 and etc.
            if fu != 25:
                fu = int(math.ceil(fu / 10.0)) * 10

            if han == 0 or (han == 1 and fu < 30):
                error = 'Not valid han ({0}) and fu ({1})'.format(han, fu)
                cost = None
            else:
                cost = self.calculate_scores(han, fu, is_tsumo, is_dealer)

            calculated_hand = {
                'cost': cost,
                'error': error,
                'hand_yaku': hand_yaku,
                'han': han,
                'fu': fu
            }
            calculated_hands.append(calculated_hand)

        # exception hand
        if not is_open_hand and self.is_kokushi(tiles_34):
            if tiles_34[win_tile // 4] == 2:
                han = yaku.daburu_kokushi.han['closed']
            else:
                han = yaku.kokushi.han['closed']
            fu = 0
            cost = self.calculate_scores(han, fu, is_tsumo, is_dealer)
            calculated_hands.append({
                'cost': cost,
                'error': None,
                'hand_yaku': [yaku.kokushi],
                'han': han,
                'fu': fu
            })

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

        return return_response()