示例#1
0
    def should_call_riichi(self):
        # empty waiting can be found in some cases
        if not self.waiting:
            return False

        if self.in_defence:
            return False

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

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

        tiles_34 = TilesConverter.to_34_array(tiles)

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

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

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

        return True
示例#2
0
    def is_condition_met(self, hand, *args):
        dragons = [CHUN, HAKU, HATSU]
        count_of_conditions = 0
        for item in hand:
            # dragon pon or pair
            if (is_pair(item) or is_pon(item)) and item[0] in dragons:
                count_of_conditions += 1

        return count_of_conditions == 3
示例#3
0
    def is_shosangen(self, hand):
        """
        Hand with two dragon pon sets and one dragon pair
        :param hand: list of hand's sets
        :return: true|false
        """
        dragons = [CHUN, HAKU, HATSU]
        count_of_conditions = 0
        for item in hand:
            # dragon pon or pair
            if (is_pair(item) or is_pon(item)) and item[0] in dragons:
                count_of_conditions += 1

        return count_of_conditions == 3
示例#4
0
    def is_condition_met(self, hand, *args):
        pon_sets = [x for x in hand if is_pon(x)]
        if len(pon_sets) < 3:
            return False

        count_of_wind_sets = 0
        wind_pair = 0
        winds = [EAST, SOUTH, WEST, NORTH]
        for item in hand:
            if is_pon(item) and item[0] in winds:
                count_of_wind_sets += 1

            if is_pair(item) and item[0] in winds:
                wind_pair += 1

        return count_of_wind_sets == 3 and wind_pair == 1
示例#5
0
    def is_shosuushi(self, hand):
        """
        The hand contains three sets of winds and a pair of the remaining wind.
        :param hand: list of hand's sets
        :return: true|false
        """
        pon_sets = [x for x in hand if is_pon(x)]
        if len(pon_sets) < 3:
            return False

        count_of_wind_sets = 0
        wind_pair = 0
        winds = [EAST, SOUTH, WEST, NORTH]
        for item in hand:
            if is_pon(item) and item[0] in winds:
                count_of_wind_sets += 1

            if is_pair(item) and item[0] in winds:
                wind_pair += 1

        return count_of_wind_sets == 3 and wind_pair == 1
示例#6
0
    def calculate_fu(
        self,
        hand,
        win_tile,
        win_group,
        config,
        valued_tiles=None,
        melds=None,
    ):
        """
        Calculate hand fu with explanations
        :param hand:
        :param win_tile: 136 tile format
        :param win_group: one set where win tile exists
        :param config: HandConfig object
        :param valued_tiles: dragons, player wind, round wind
        :param melds: opened sets
        :return:
        """

        win_tile_34 = win_tile // 4

        if not valued_tiles:
            valued_tiles = []

        if not melds:
            melds = []

        fu_details = []

        if len(hand) == 7:
            return [{"fu": 25, "reason": FuCalculator.BASE}], 25

        pair = [x for x in hand if is_pair(x)][0]
        pon_sets = [x for x in hand if is_pon_or_kan(x)]

        copied_opened_melds = [x.tiles_34 for x in melds if x.type == Meld.CHI]
        closed_chi_sets = []
        for x in hand:
            if x not in copied_opened_melds:
                closed_chi_sets.append(x)
            else:
                copied_opened_melds.remove(x)

        is_open_hand = any([x.opened for x in melds])

        if win_group in closed_chi_sets:
            tile_index = simplify(win_tile_34)

            # penchan
            if contains_terminals(win_group):
                # 1-2-... wait
                if tile_index == 2 and win_group.index(win_tile_34) == 2:
                    fu_details.append({
                        "fu": 2,
                        "reason": FuCalculator.PENCHAN
                    })
                # 8-9-... wait
                elif tile_index == 6 and win_group.index(win_tile_34) == 0:
                    fu_details.append({
                        "fu": 2,
                        "reason": FuCalculator.PENCHAN
                    })

            # kanchan waiting 5-...-7
            if win_group.index(win_tile_34) == 1:
                fu_details.append({"fu": 2, "reason": FuCalculator.KANCHAN})

        # valued pair
        count_of_valued_pairs = valued_tiles.count(pair[0])
        if count_of_valued_pairs == 1:
            fu_details.append({"fu": 2, "reason": FuCalculator.VALUED_PAIR})

        # east-east pair when you are on east gave double fu
        if count_of_valued_pairs == 2:
            fu_details.append({
                "fu": 4,
                "reason": FuCalculator.DOUBLE_VALUED_PAIR
            })

        # pair wait
        if is_pair(win_group):
            fu_details.append({"fu": 2, "reason": FuCalculator.PAIR_WAIT})

        for set_item in pon_sets:
            open_meld = [x for x in melds if set_item == x.tiles_34]
            open_meld = open_meld and open_meld[0] or None

            set_was_open = open_meld and open_meld.opened or False
            is_kan_set = (open_meld and
                          (open_meld.type == Meld.KAN
                           or open_meld.type == Meld.SHOUMINKAN)) or False
            is_honor = set_item[0] in TERMINAL_INDICES + HONOR_INDICES

            # we win by ron on the third pon tile, our pon will be count as open
            if not config.is_tsumo and set_item == win_group:
                set_was_open = True

            if is_honor:
                if is_kan_set:
                    if set_was_open:
                        fu_details.append({
                            "fu":
                            16,
                            "reason":
                            FuCalculator.OPEN_TERMINAL_KAN
                        })
                    else:
                        fu_details.append({
                            "fu":
                            32,
                            "reason":
                            FuCalculator.CLOSED_TERMINAL_KAN
                        })
                else:
                    if set_was_open:
                        fu_details.append({
                            "fu":
                            4,
                            "reason":
                            FuCalculator.OPEN_TERMINAL_PON
                        })
                    else:
                        fu_details.append({
                            "fu":
                            8,
                            "reason":
                            FuCalculator.CLOSED_TERMINAL_PON
                        })
            else:
                if is_kan_set:
                    if set_was_open:
                        fu_details.append({
                            "fu": 8,
                            "reason": FuCalculator.OPEN_KAN
                        })
                    else:
                        fu_details.append({
                            "fu": 16,
                            "reason": FuCalculator.CLOSED_KAN
                        })
                else:
                    if set_was_open:
                        fu_details.append({
                            "fu": 2,
                            "reason": FuCalculator.OPEN_PON
                        })
                    else:
                        fu_details.append({
                            "fu": 4,
                            "reason": FuCalculator.CLOSED_PON
                        })

        add_tsumo_fu = len(fu_details) > 0 or config.options.fu_for_pinfu_tsumo

        if config.is_tsumo and add_tsumo_fu:
            # 2 additional fu for tsumo (but not for pinfu)
            fu_details.append({"fu": 2, "reason": FuCalculator.TSUMO})

        if is_open_hand and not len(
                fu_details) and config.options.fu_for_open_pinfu:
            # there is no 1-20 hands, so we had to add additional fu
            fu_details.append({
                "fu": 2,
                "reason": FuCalculator.HAND_WITHOUT_FU
            })

        if is_open_hand or config.is_tsumo:
            fu_details.append({"fu": 20, "reason": FuCalculator.BASE})
        else:
            fu_details.append({"fu": 30, "reason": FuCalculator.BASE})

        return fu_details, self.round_fu(fu_details)
示例#7
0
    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
示例#8
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
    def _choose_best_discard_in_tempai(self, discard_options, after_meld):
        discard_desc = []

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

        for discard_option in discard_options:
            call_riichi = discard_option.with_riichi
            tiles_original, discard_original = self.emulate_discard(
                discard_option)

            is_furiten = self._is_discard_option_furiten(discard_option)

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

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

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

                tanki_type = None

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

                    if is_pair(hand_set):
                        is_tanki = True

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

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

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

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

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

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

            # reverse all temporary tile tweaks
            self.restore_after_emulate_discard(tiles_original,
                                               discard_original)

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

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

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

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

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

        # if we have several options that give us similar wait
        return best_discard_desc[0]["discard_option"]
示例#10
0
    def _choose_best_discard_in_tempai(self, tiles, melds, discard_options):
        # first of all we find tiles that have the best hand cost * ukeire value
        call_riichi = not self.player.is_open_hand

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

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

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

            is_furiten = self._is_discard_option_furiten(discard_option)

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

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

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

                tanki_type = None

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

                    if is_pair(hand_set):
                        is_tanki = True

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

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

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

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

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

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

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

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

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

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

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

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

        # if we have several options that give us similar wait
        # FIXME: 1. we find the safest tile to discard
        # FIXME: 2. if safeness is the same, we try to discard non-dora tiles
        return best_discard_desc[0]['discard_option']
示例#11
0
    def should_call_riichi(self):
        logger.info("Can call a reach!")

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

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

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

            lose_estimation = 6000 if self.player.is_dealer else 7000

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

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

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

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

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

        should_attack = not self.defence.should_go_to_defence_mode()

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

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

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

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


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

        tiles_34 = TilesConverter.to_34_array(tiles)

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

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

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

        return True
    def _choose_best_discard_in_tempai(self, tiles, melds, discard_options):
        # first of all we find tiles that have the best hand cost * ukeire value
        call_riichi = not self.player.is_open_hand

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

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

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

            is_furiten = self._is_discard_option_furiten(discard_option)

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

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

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

                tanki_type = None

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

                    if is_pair(hand_set):
                        is_tanki = True

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

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

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

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

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

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

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

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

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

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

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

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

        # if we have several options that give us similar wait
        # FIXME: 1. we find the safest tile to discard
        # FIXME: 2. if safeness is the same, we try to discard non-dora tiles
        return best_discard_desc[0]['discard_option']
示例#13
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