Esempio n. 1
0
    def _initialize_honitsu_dora_count(self, tiles_136, suit):
        tiles_34 = TilesConverter.to_34_array(tiles_136)

        dora_count_man_not_isolated = 0
        dora_count_pin_not_isolated = 0
        dora_count_sou_not_isolated = 0

        for tile_136 in tiles_136:
            tile_34 = tile_136 // 4

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

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

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

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

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

        if suit['name'] == 'pin':
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated
        elif suit['name'] == 'sou':
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated
        elif suit['name'] == 'man':
            self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated
    def calculate_dora_count(self, tiles_136):
        self.dora_count_central = 0
        self.dora_count_not_central = 0
        self.aka_dora_count = 0

        for tile_136 in tiles_136:
            tile_34 = tile_136 // 4

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

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

            if not dora_count:
                continue

            if is_honor(tile_34):
                self.dora_count_not_central += dora_count
                self.dora_count_honor += dora_count
            elif is_terminal(tile_34):
                self.dora_count_not_central += dora_count
            else:
                self.dora_count_central += dora_count

        self.dora_count_central += self.aka_dora_count
        self.dora_count_total = self.dora_count_central + self.dora_count_not_central
Esempio n. 3
0
    def calculate_dora_count(self, tiles_136):
        self.dora_count_central = 0
        self.dora_count_not_central = 0
        self.aka_dora_count = 0

        for tile_136 in tiles_136:
            tile_34 = tile_136 // 4

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

            if not dora_count:
                continue

            if is_honor(tile_34):
                self.dora_count_not_central += dora_count
                self.dora_count_honor += dora_count
            elif is_terminal(tile_34):
                self.dora_count_not_central += dora_count
            else:
                self.dora_count_central += dora_count

        self.dora_count_central += self.aka_dora_count
        self.dora_count_total = self.dora_count_central + self.dora_count_not_central
Esempio n. 4
0
    def get_bonus_danger(self, tile_136, number_of_revealed_tiles):
        bonus_danger = []
        tile_34 = tile_136 // 4
        number_of_yakuhai = self.enemy.valued_honors.count(tile_34)

        # shonpai tiles
        if number_of_revealed_tiles == 1:
            # aka doras don't get additional danger against toitoi, they just get their regular one
            dora_count = plus_dora(tile_136, self.enemy.table.dora_indicators)
            if dora_count > 0:
                danger = copy(TileDanger.TOITOI_SHONPAI_DORA_BONUS_DANGER)
                danger["value"] = dora_count * danger["value"]
                danger["dora_count"] = dora_count
                bonus_danger.append(danger)

            if number_of_yakuhai > 0:
                bonus_danger.append(TileDanger.TOITOI_SHONPAI_YAKUHAI_BONUS_DANGER)
            else:
                bonus_danger.append(TileDanger.TOITOI_SHONPAI_NON_YAKUHAI_BONUS_DANGER)
        elif number_of_revealed_tiles == 2:
            if number_of_yakuhai > 0:
                bonus_danger.append(TileDanger.TOITOI_SECOND_YAKUHAI_HONOR_BONUS_DANGER)
        elif number_of_revealed_tiles == 3:
            # FIXME: we should add negative bonus danger exclusively against toitoi for such tiles
            # except for doras and honors maybe
            pass

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

        dora_count_man_not_isolated = 0
        dora_count_pin_not_isolated = 0
        dora_count_sou_not_isolated = 0

        for tile_136 in tiles_136:
            tile_34 = tile_136 // 4

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

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

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

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

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

        if suit['name'] == 'pin':
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated
        elif suit['name'] == 'sou':
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated
        elif suit['name'] == 'man':
            self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated
Esempio n. 6
0
    def is_threatening(self):
        """
        Should we fold against this player or not
        :return: boolean
        """
        if self.player.in_riichi:
            return True

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

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

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

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

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

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

        return False
Esempio n. 7
0
    def calculate_value(self, shanten=None):
        # base is 100 for ability to mark tiles as not needed (like set value to 50)
        value = 100
        honored_value = 20

        # we don't need to keep honor tiles in almost completed hand
        if shanten and shanten <= 2:
            honored_value = 0

        if is_honor(self.tile_to_discard):
            if self.tile_to_discard in self.player.valued_honors:
                count_of_winds = [
                    x for x in self.player.valued_honors
                    if x == self.tile_to_discard
                ]
                # for west-west, east-east we had to double tile value
                value += honored_value * len(count_of_winds)
        else:
            # suits
            suit_tile_grades = [10, 20, 30, 40, 50, 40, 30, 20, 10]
            simplified_tile = simplify(self.tile_to_discard)
            value += suit_tile_grades[simplified_tile]

        count_of_dora = plus_dora(self.tile_to_discard * 4,
                                  self.player.table.dora_indicators)
        if is_aka_dora(self.tile_to_discard * 4,
                       self.player.table.has_open_tanyao):
            count_of_dora += 1

        value += 50 * count_of_dora

        if is_honor(self.tile_to_discard):
            # depends on how much honor tiles were discarded
            # we will decrease tile value
            discard_percentage = [100, 75, 20, 0, 0]
            discarded_tiles = self.player.table.revealed_tiles[
                self.tile_to_discard]

            value = (value * discard_percentage[discarded_tiles]) / 100

            # three honor tiles were discarded,
            # so we don't need this tile anymore
            if value == 0:
                self.had_to_be_discarded = True

        self.valuation = value
Esempio n. 8
0
    def _initialize_honitsu_dora_count(self, tiles_136, suit):
        tiles_34 = TilesConverter.to_34_array(tiles_136)

        dora_count_man = 0
        dora_count_pin = 0
        dora_count_sou = 0

        dora_count_man_not_isolated = 0
        dora_count_pin_not_isolated = 0
        dora_count_sou_not_isolated = 0

        for tile_136 in tiles_136:
            tile_34 = tile_136 // 4

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

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

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

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

        if suit["name"] == "pin":
            self.dora_count_our_suit = dora_count_pin
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated
        elif suit["name"] == "sou":
            self.dora_count_our_suit = dora_count_sou
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated
        elif suit["name"] == "man":
            self.dora_count_our_suit = dora_count_man
            self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated
Esempio n. 9
0
    def _get_dora_scale_bonus(self, tile_136):
        tile_34 = tile_136 // 4
        scale_bonus = 0

        dora_count = plus_dora(tile_136,
                               self.table.dora_indicators,
                               add_aka_dora=self.table.has_aka_dora)

        if is_honor(tile_34):
            closed_hand_34 = TilesConverter.to_34_array(
                self.main_player.closed_hand)
            revealed_tiles = self.main_player.number_of_revealed_tiles(
                tile_34, closed_hand_34)
            if revealed_tiles < 2:
                scale_bonus += dora_count * 3
            else:
                scale_bonus += dora_count * 2
        else:
            scale_bonus += dora_count

        return scale_bonus
    def is_threatening(self):
        """
        Should we fold against this player or not
        :return: boolean
        """
        if self.player.in_riichi:
            return True

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

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

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

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

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

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

        return False
    def _simplified_danger_valuation(self, discard_option):
        tile_34 = discard_option.tile_to_discard_34
        tile_136 = discard_option.tile_to_discard_136
        number_of_revealed_tiles = self.player.number_of_revealed_tiles(
            tile_34, TilesConverter.to_34_array(self.player.closed_hand))
        if is_honor(tile_34):
            if not self.player.table.is_common_yakuhai(tile_34):
                if number_of_revealed_tiles == 4:
                    simple_danger = 0
                elif number_of_revealed_tiles == 3:
                    simple_danger = 10
                elif number_of_revealed_tiles == 2:
                    simple_danger = 20
                else:
                    simple_danger = 30
            else:
                if number_of_revealed_tiles == 4:
                    simple_danger = 0
                elif number_of_revealed_tiles == 3:
                    simple_danger = 11
                elif number_of_revealed_tiles == 2:
                    simple_danger = 21
                else:
                    simple_danger = 32
        elif is_terminal(tile_34):
            simple_danger = 100
        elif simplify(tile_34) < 2 or simplify(tile_34) > 6:
            # 2, 3 or 7, 8
            simple_danger = 200
        else:
            # 4, 5, 6
            simple_danger = 300

        if simple_danger != 0:
            simple_danger += plus_dora(
                tile_136,
                self.player.table.dora_indicators,
                add_aka_dora=self.player.table.has_aka_dora)

        return simple_danger
Esempio n. 12
0
    def _suji_tiles(self, suji):
        suji_temp = suji % 9
        base = suji - suji_temp - 1

        first_danger = 20
        second_danger = 30
        third_danger = 40

        result = []
        if suji_temp == self.FIRST_SUJI:
            result = [
                DefenceTile(base + 1, first_danger),
                DefenceTile(base + 4, second_danger),
                DefenceTile(base + 7, third_danger)
            ]

        if suji_temp == self.SECOND_SUJI:
            result = [
                DefenceTile(base + 2, second_danger),
                DefenceTile(base + 5, second_danger),
                DefenceTile(base + 8, second_danger)
            ]

        if suji_temp == self.THIRD_SUJI:
            result = [
                DefenceTile(base + 3, third_danger),
                DefenceTile(base + 6, second_danger),
                DefenceTile(base + 9, first_danger)
            ]

        # mark dora tiles as dangerous tiles to discard
        for tile in result:
            is_dora = plus_dora(tile.value * 4, self.table.dora_indicators) \
                      or is_aka_dora(tile.value * 4, self.table.has_open_tanyao)
            if is_dora:
                tile.danger += 100

        return result
Esempio n. 13
0
    def _suji_tiles(self, suji):
        suji_temp = suji % 9
        base = suji - suji_temp - 1

        first_danger = 20
        second_danger = 30
        third_danger = 40

        result = []
        if suji_temp == self.FIRST_SUJI:
            result = [
                DefenceTile(base + 1, first_danger),
                DefenceTile(base + 4, second_danger),
                DefenceTile(base + 7, third_danger)
            ]

        if suji_temp == self.SECOND_SUJI:
            result = [
                DefenceTile(base + 2, second_danger),
                DefenceTile(base + 5, second_danger),
                DefenceTile(base + 8, second_danger)
            ]

        if suji_temp == self.THIRD_SUJI:
            result = [
                DefenceTile(base + 3, third_danger),
                DefenceTile(base + 6, second_danger),
                DefenceTile(base + 9, first_danger)
            ]

        # mark dora tiles as dangerous tiles to discard
        for tile in result:
            is_dora = plus_dora(tile.value * 4, self.table.dora_indicators) \
                      or is_aka_dora(tile.value * 4, self.table.has_open_tanyao)
            if is_dora:
                tile.danger += 100

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

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

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

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

        suit = suits[0]

        count_of_shuntsu_other_suits = 0
        count_of_koutsu_other_suits = 0

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.chosen_suit = suit['function']

        return True
Esempio n. 15
0
    def should_go_to_defence_mode(self, discard_candidate=None):
        """
        The method is decides should bot go to the defence mode or not.
        For now only full defence is possible
        :return: true|false
        """

        # we drew a tile, so we have 14 tiles in our hand
        if discard_candidate:
            shanten = discard_candidate.shanten
            waiting = discard_candidate.waiting
            wanted_tiles_count = discard_candidate.tiles_count
        # we have 13 tiles in hand (this is not our turn)
        else:
            shanten = self.player.ai.previous_shanten
            waiting = self.player.ai.waiting
            wanted_tiles_count = self.player.ai.wanted_tiles_count

        # if we are the top, it's better to defence
        # if self.player == self.table.get_players_sorted_by_scores()[0] and self.player.scores > 30000:
        #     logger.info("Player is at the 1st position, better to fold.")
        #     return True

        # if we are in riichi or meld too much, we can't defence
        if self.player.in_riichi or self.player.ai.pushing or len(
                self.player.melds) >= 3:
            logger.info("In reach or pushing state, cannot defence.")
            return False

        # if we are the at the 4st position, it's better to push
        if self.player == self.table.get_players_sorted_by_scores(
        )[-1] and self.table.round_number >= 5:
            logger.info("Player is at the 4st position, better to push.")
            self.player.ai.pushing = True
            self.player.set_state("PUSHING")
            return False

        threatening_players = self._get_threatening_players()
        self.threatening_players = threatening_players  # assign this for further calculation in other methods

        # no one is threatening, so we can build our hand
        if len(threatening_players) == 0:
            return False
        else:
            #logger.info("There are some threatening players! Now shanten is {}".format(shanten))
            pass

        # more than 2 players are threatening, so defense is better
        #if len(threatening_players) >= 2:
        #    logger.info("Watch those players feed each other!")
        #    return True

        if shanten == 1:
            #     # When player is in 4th position, it's better to push in this situation
            #     if self.player == self.table.get_players_sorted_by_scores()[-1]:
            #         logger.info("Player is in 4th position, better to push.")
            #         return False

            # TODO calculate all possible hand costs for 1-2 shanten
            dora_count = sum([
                plus_dora(x, self.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.table.has_open_tanyao)
            ])
            # we had 3+ dora in our almost done hand,
            # we can try to push it
            if dora_count >= 3:
                return False

        # our hand is not tempai, so better to fold it
        if shanten != 0:
            #logger.info("Not prepared, ready to fold.")
            return True

        # we are in tempai, if there are so many melds, don't go to defence
        # if len(self.player.melds) >= 2:
        #     logger.info("Too many melds, should push.")
        #     return False

        # we are in tempai, let's try to estimate hand value
        hands_estimated_cost = []
        call_riichi = not self.player.is_open_hand
        for tile in waiting:
            # copy of tiles, because we are modifying a list
            tiles = self.player.tiles[:]

            # special case, when we already have 14 tiles in the hand
            if discard_candidate:
                temp_tile = discard_candidate.find_tile_in_hand(
                    self.player.closed_hand)
                tiles.remove(temp_tile)

            hand_result = self.player.ai.estimate_hand_value(
                tile, tiles, call_riichi)
            if hand_result.error is None:
                hands_estimated_cost.append(hand_result.cost['main'])

        # probably we are with opened hand without yaku, let's fold it
        if not hands_estimated_cost:
            logger.info("This hand cannot win, fold it.")
            return True

        # Get the hand value
        hand_value = sum(hands_estimated_cost) / len(hands_estimated_cost)
        hand_value += self.table.count_of_riichi_sticks * 1000
        if self.player.is_dealer:
            hand_value += 700  # EV for dealer combo
        # EH: makes the calculation of hand value better by adding the remaining tile count

        # Get the shape for attacking
        hand_shape = "bad_shape"
        if wanted_tiles_count > 4:
            hand_shape = "good_shape"

        # Check whether the player is in proactive mode
        if "PROACTIVE" in self.player.play_state:
            hand_shape = "pro_" + hand_shape

        # Get the current hand index
        hand_index = len(self.player.discards)

        # Get the type of threatening player
        counter_player_type = "player"
        if threatening_players[0].is_dealer:
            counter_player_type = "dealer"

        score_ev = hand_value - COUNTER_VALUES[
            counter_player_type] * COUNTER_RATIO[hand_shape][hand_index]
        rank_ev = self.get_rank_ev(hand_value,
                                   COUNTER_VALUES[counter_player_type],
                                   COUNTER_RATIO[hand_shape][hand_index])

        should_counter = False

        if self.table.round_number < 3:  # DEBUG: set this to 0 to debug rank ev calculation
            # Before Round East 4, use score ev
            if score_ev > 0:
                should_counter = True
        else:
            if rank_ev > 0:
                should_counter = True
            elif rank_ev == 0 and score_ev > 0:
                should_encounter = True

        logger.info('''Cowboy: Counter: 
            Hand Value: {}    Hand Shape: {}    
            Hand Index: {}    Counter Player Type: {}
            Score EV: {}    Rank EV: {} 
            Should Counter: {}'''.format(hand_value, hand_shape, hand_index,
                                         counter_player_type, score_ev,
                                         rank_ev, should_counter))

        if should_counter:
            # set state
            self.player.ai.waiting = waiting
            self.player.ai.wanted_tiles_count = wanted_tiles_count
            if self.player.play_state in ["PREPARING", "DEFENCE"]:
                if hand_shape == "good_shape":
                    self.player.set_state("REACTIVE_GOODSHAPE")
                else:
                    self.player.set_state("REACTIVE_BADSHAPE")

            if self.player != self.table.get_players_sorted_by_scores()[0]:
                # When player is on the top, no need to push, else push it
                self.player.ai.pushing = True

            return False
        else:
            return True

        # our open hand in tempai, but it is cheap
        # so we can fold it
        # if self.player.is_open_hand and max_cost < 7000:
        #    return True

        # when we call riichi we can get ura dora,
        # so it is reasonable to riichi 3k+ hands
        # if not self.player.is_open_hand:
        #     # there are a lot of chances that we will not win with a bad wait
        #     # against other threatening players
        #     if max_cost < 3000 or len(waiting) < 2:
        #         return True

        return False
Esempio n. 16
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()
Esempio n. 17
0
    def should_activate_strategy(self):
        """
        Tanyao hand is a hand without terminal and honor tiles, to achieve this
        we will use different approaches
        :return: boolean
        """

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

        # 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)
        ])
        # Get shanten
        shanten = self.player.ai.previous_shanten
        # Get hand index
        hand_index = len(self.player.discards)

        #if not ((dora_count >= 1) or (shanten <= 1 and hand_index >= 8) or self.player.is_dealer):
        #    return False

        if len(self.player.discards) <= 4:
            #Do not activate it too early
            return False

        tiles = TilesConverter.to_34_array(self.player.tiles)
        count_of_terminal_pon_sets = 0
        count_of_terminal_pairs = 0
        count_of_valued_pairs = 0
        for x in range(0, 34):
            tile = tiles[x]
            if not tile:
                continue

            if x in self.not_suitable_tiles and tile == 3:
                count_of_terminal_pon_sets += 1

            if x in self.not_suitable_tiles and tile == 2:
                count_of_terminal_pairs += 1

                if x in self.player.valued_honors:
                    count_of_valued_pairs += 1

        # if we already have pon of honor\terminal tiles
        # we don't need to open hand for tanyao
        if count_of_terminal_pon_sets > 0:
            return False

        # with valued pair (yakuhai wind or dragon)
        # we don't need to go for tanyao
        if count_of_valued_pairs > 0:
            return False

        # no pair is ok in tanyao pair
        # but 1+ pairs can't be suitable
        #if count_of_terminal_pairs >= 1:
        #    return False

        # 1234 and 9876 indices
        indices = [
            [0, 1, 2, 3],
            [8, 7, 6, 5],
            [9, 10, 11, 12],
            [17, 16, 15, 14],
            [18, 19, 20, 21],
            [26, 25, 24, 23],
        ]

        num_terminal_lugs = 0
        for index_set in indices:
            first = int(tiles[index_set[0]] >= 1)
            second = int(tiles[index_set[1]] >= 1)
            third = int(tiles[index_set[2]] >= 1)
            fourth = int(tiles[index_set[3]] >= 1)
            if ((first + second >= 2) or (first + third >= 2) or
                (second + third >= 2)) and fourth == 0:
                num_terminal_lugs += 1
            if first + second + third >= 3:
                num_terminal_lugs += 2
        num_terminal_lugs += count_of_terminal_pairs

        if num_terminal_lugs > 1:
            return False

        return True
Esempio n. 18
0
 def is_dora(self, tile):
     """
     tile in 136 format
     """
     return plus_dora(tile, self.dora_indicators) or is_aka_dora(tile, True)
Esempio n. 19
0
 def is_dora(self, tile):
     return plus_dora(tile, self.dora_indicators) or is_aka_dora(tile, self.has_open_tanyao)
Esempio n. 20
0
 def is_dora(self, tile):
     return plus_dora(tile, self.dora_indicators) or is_aka_dora(tile)
Esempio n. 21
0
    def calculate_tiles_danger(
        self, discard_candidates: List[DiscardOption], enemy_analyzer: EnemyAnalyzer
    ) -> List[DiscardOption]:
        closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)

        safe_against_threat_34 = []

        # First, add all genbutsu to the list
        safe_against_threat_34.extend(list(set([x for x in enemy_analyzer.enemy.all_safe_tiles])))

        # Then add tiles not suitable for yaku in enemy open hand
        if enemy_analyzer.threat_reason.get("active_yaku"):
            safe_against_yaku = set.intersection(
                *[set(x.get_safe_tiles_34()) for x in enemy_analyzer.threat_reason.get("active_yaku")]
            )
            if safe_against_yaku:
                safe_against_threat_34.extend(list(safe_against_yaku))

        possible_forms = self.possible_forms_analyzer.calculate_possible_forms(enemy_analyzer.enemy.all_safe_tiles)
        kabe_tiles = self.player.ai.kabe.find_all_kabe(closed_hand_34)
        suji_tiles = self.player.ai.suji.find_suji([x.value for x in enemy_analyzer.enemy.discards])
        for discard_option in discard_candidates:
            tile_34 = discard_option.tile_to_discard_34
            tile_136 = discard_option.find_tile_in_hand(self.player.closed_hand)
            number_of_revealed_tiles = self.player.number_of_revealed_tiles(tile_34, closed_hand_34)

            # like 1-9 against tanyao etc.
            if tile_34 in safe_against_threat_34:
                self._update_discard_candidate(
                    tile_34,
                    discard_candidates,
                    enemy_analyzer.enemy.seat,
                    TileDanger.SAFE_AGAINST_THREATENING_HAND,
                )
                continue

            # safe tiles that can be safe based on the table situation
            if self.total_possible_forms_for_tile(possible_forms, tile_34) == 0:
                self._update_discard_candidate(
                    tile_34,
                    discard_candidates,
                    enemy_analyzer.enemy.seat,
                    TileDanger.IMPOSSIBLE_WAIT,
                )
                continue

            # honors
            if is_honor(tile_34):
                danger = self._process_danger_for_honor(enemy_analyzer, tile_34, number_of_revealed_tiles)
            # terminals
            elif is_terminal(tile_34):
                danger = self._process_danger_for_terminal_tiles_and_kabe_suji(
                    enemy_analyzer, tile_34, number_of_revealed_tiles, kabe_tiles, suji_tiles
                )
            # 2-8 tiles
            else:
                danger = self._process_danger_for_2_8_tiles_suji_and_kabe(
                    enemy_analyzer, tile_34, number_of_revealed_tiles, suji_tiles, kabe_tiles
                )

            if danger:
                self._update_discard_candidate(
                    tile_34,
                    discard_candidates,
                    enemy_analyzer.enemy.seat,
                    danger,
                )

            forms_count = possible_forms[tile_34]
            self._update_discard_candidate(
                tile_34,
                discard_candidates,
                enemy_analyzer.enemy.seat,
                {
                    "value": self.possible_forms_analyzer.calculate_possible_forms_danger(forms_count),
                    "description": TileDanger.FORM_BONUS_DESCRIPTION,
                    "forms_count": forms_count,
                },
            )

            # for ryanmen waits we also account for number of dangerous suji tiles
            forms_ryanmen_count = forms_count[PossibleFormsAnalyzer.POSSIBLE_RYANMEN_SIDES]
            if forms_ryanmen_count == 1:
                self._update_discard_candidate(
                    tile_34,
                    discard_candidates,
                    enemy_analyzer.enemy.seat,
                    TileDanger.RYANMEN_BASE_SINGLE,
                )
            elif forms_ryanmen_count == 2:
                self._update_discard_candidate(
                    tile_34,
                    discard_candidates,
                    enemy_analyzer.enemy.seat,
                    TileDanger.RYANMEN_BASE_DOUBLE,
                )

            if forms_ryanmen_count == 1 or forms_ryanmen_count == 2:
                has_matagi = self._is_matagi_suji(enemy_analyzer, tile_34)
                if has_matagi:
                    self._update_discard_candidate(
                        tile_34,
                        discard_candidates,
                        enemy_analyzer.enemy.seat,
                        TileDanger.BONUS_MATAGI_SUJI,
                        can_be_used_for_ryanmen=True,
                    )

                has_aidayonken = self.is_aidayonken_pattern(enemy_analyzer, tile_34)
                if has_aidayonken:
                    self._update_discard_candidate(
                        tile_34,
                        discard_candidates,
                        enemy_analyzer.enemy.seat,
                        TileDanger.BONUS_AIDAYONKEN,
                        can_be_used_for_ryanmen=True,
                    )

                early_danger_bonus = self._get_early_danger_bonus(enemy_analyzer, tile_34, has_matagi or has_aidayonken)
                if early_danger_bonus is not None:
                    self._update_discard_candidate(
                        tile_34,
                        discard_candidates,
                        enemy_analyzer.enemy.seat,
                        early_danger_bonus,
                        can_be_used_for_ryanmen=True,
                    )

                self._update_discard_candidate(
                    tile_34,
                    discard_candidates,
                    enemy_analyzer.enemy.seat,
                    TileDanger.make_unverified_suji_coeff(enemy_analyzer.unverified_suji_coeff),
                    can_be_used_for_ryanmen=True,
                )

                if is_dora_connector(tile_136, self.player.table.dora_indicators):
                    self._update_discard_candidate(
                        tile_34,
                        discard_candidates,
                        enemy_analyzer.enemy.seat,
                        TileDanger.DORA_CONNECTOR_BONUS,
                        can_be_used_for_ryanmen=True,
                    )

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

            if dora_count > 0:
                danger = copy(TileDanger.DORA_BONUS)
                danger["value"] = dora_count * danger["value"]
                danger["dora_count"] = dora_count
                self._update_discard_candidate(
                    tile_34,
                    discard_candidates,
                    enemy_analyzer.enemy.seat,
                    danger,
                )

            if enemy_analyzer.threat_reason.get("active_yaku"):
                for yaku_analyzer in enemy_analyzer.threat_reason.get("active_yaku"):
                    bonus_danger = yaku_analyzer.get_bonus_danger(tile_136, number_of_revealed_tiles)
                    for danger in bonus_danger:
                        self._update_discard_candidate(
                            tile_34,
                            discard_candidates,
                            enemy_analyzer.enemy.seat,
                            danger,
                        )

        return discard_candidates
Esempio n. 22
0
    def should_activate_strategy(self, tiles_136, meld_tile=None):
        """
        We can go for honitsu strategy if we have prevalence of one suit and honor tiles
        """

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

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

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

        suit = suits[0]

        count_of_shuntsu_other_suits = 0
        count_of_koutsu_other_suits = 0
        count_of_ryanmen_other_suits = 0

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

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

        count_of_ryanmen_other_suits += self._find_ryanmen_waits(tiles_34, suits[1]["function"])
        count_of_ryanmen_other_suits += self._find_ryanmen_waits(tiles_34, suits[2]["function"])

        self._calculate_suitable_and_not_suitable_tiles_cnt(tiles_34, suit["function"])
        self._initialize_honitsu_dora_count(tiles_136, suit)

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

        # 7th turn and still 4 tiles in other suits - meh
        if self.tiles_count_other_suits >= 4 and self.player.round_step > 6:
            return False

        # 12th turn is too late and we still have too many tiles in other suits
        if self.tiles_count_other_suits >= 3 and self.player.round_step > 11:
            return False

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

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

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

        # what's the point of honitsu if there is not a single honor pair
        if honor_pairs_or_pons == 0:
            return False

        # if we have twu ryanmens in other suits
        if count_of_ryanmen_other_suits >= 2:
            return False

        # let's not go for honitsu nomi
        if not valued_pairs and not valued_pons:
            # this is not honitsu, maybe it will be pinfu one day
            if self.tiles_count_our_suit <= 7 and honor_pairs_or_pons < 2:
                return False

            # also looks more like pinfu
            if self.tiles_count_other_suits >= 4:
                return False

            # so-so, let's just not go for honitsu nomi
            if self.tiles_count_our_suit <= 9 and honor_pairs_or_pons == 1:
                if not self.dora_count_our_suit and not honor_doras_pairs_or_pons:
                    return False

        # if we have some decent amount of not isolated tiles in other suits
        # we may not rush for honitsu considering other conditions
        if self.tiles_count_other_suits_not_isolated >= 3:
            # if we don't have pair or pon of honored doras
            if honor_doras_pairs_or_pons == 0:
                # if we have a ryanmen with dora in other suit and no honor doras, so let's not rush honitsu
                if count_of_ryanmen_other_suits >= 1 and self.dora_count_other_suits_not_isolated >= 1:
                    return False

                # we need to either have a valued pair or have at least two honor
                # pairs to consider honitsu
                if valued_pairs == 0 and honor_pairs_or_pons < 2:
                    return False

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

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

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

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

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

        self.chosen_suit = suit["function"]

        return True
Esempio n. 23
0
    def should_go_to_defence_mode(self, discard_candidate=None):
        """
        The method is decides should bot go to the defence mode or not.
        For now only full defence is possible
        :return: true|false
        """

        # we drew a tile, so we have 14 tiles in our hand
        if discard_candidate:
            shanten = discard_candidate.shanten
            waiting = discard_candidate.waiting
        # we have 13 tiles in hand (this is not our turn)
        else:
            shanten = self.player.ai.shanten
            waiting = self.player.ai.waiting

        if not waiting:
            waiting = []

        # if we are in riichi, we can't defence
        if self.player.in_riichi:
            return False

        threatening_players = self._get_threatening_players()

        # no one is threatening, so we can build our hand
        if len(threatening_players) == 0:
            return False

        if shanten == 1:
            # TODO calculate all possible hand costs for 1-2 shanten
            dora_count = sum([
                plus_dora(x, self.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.table.has_open_tanyao)
            ])
            # we had 3+ dora in our almost done hand,
            # we can try to push it
            if dora_count >= 3:
                return False

        # our hand is not tempai, so better to fold it
        if shanten != 0:
            return True

        # we are in tempai, let's try to estimate hand value
        hands_estimated_cost = []
        call_riichi = not self.player.is_open_hand
        for tile in waiting:
            # copy of tiles, because we are modifying a list
            tiles = self.player.tiles[:]

            # special case, when we already have 14 tiles in the hand
            if discard_candidate:
                temp_tile = discard_candidate.find_tile_in_hand(
                    self.player.closed_hand)
                tiles.remove(temp_tile)

            hand_result = self.player.ai.estimate_hand_value(
                tile, tiles, call_riichi)
            if hand_result.error is None:
                hands_estimated_cost.append(hand_result.cost['main'])

        # probably we are with opened hand without yaku, let's fold it
        if not hands_estimated_cost:
            return True

        max_cost = max(hands_estimated_cost)
        # our open hand in tempai, but it is cheap
        # so we can fold it
        if self.player.is_open_hand and max_cost < 7000:
            return True

        # when we call riichi we can get ura dora,
        # so it is reasonable to riichi 3k+ hands
        if not self.player.is_open_hand:
            # there are a lot of chances that we will not win with a bad wait
            # against other threatening players
            if max_cost < 3000 or len(waiting) < 2:
                return True

        return False
Esempio n. 24
0
    def collect_stat_for_enemy_riichi_hand_cost(tile_136, enemy, main_player):
        tile_34 = tile_136 // 4

        riichi_discard = [x for x in enemy.discards if x.riichi_discard]
        if riichi_discard:
            assert len(riichi_discard) == 1
            riichi_discard = riichi_discard[0]
        else:
            # FIXME: it happens when user called riichi and we are trying to decide to we need to open hand on
            # riichi tile or not. We need to process this situation correctly.
            riichi_discard = enemy.discards[-1]

        riichi_called_on_step = enemy.discards.index(riichi_discard) + 1

        total_dora_in_game = len(enemy.table.dora_indicators) * 4 + (
            3 * int(enemy.table.has_aka_dora))
        visible_tiles = enemy.table.revealed_tiles_136 + main_player.closed_hand
        visible_dora_tiles = sum([
            plus_dora(x,
                      enemy.table.dora_indicators,
                      add_aka_dora=enemy.table.has_aka_dora)
            for x in visible_tiles
        ])
        live_dora_tiles = total_dora_in_game - visible_dora_tiles
        assert live_dora_tiles >= 0, "Live dora tiles can't be less than 0"

        number_of_kan_in_enemy_hand = 0
        number_of_dora_in_enemy_kan_sets = 0
        number_of_yakuhai_enemy_kan_sets = 0
        for meld in enemy.melds:
            # if he is in riichi he can only have closed kan
            assert meld.type == MeldPrint.KAN and not meld.opened

            number_of_kan_in_enemy_hand += 1

            for tile in meld.tiles:
                number_of_dora_in_enemy_kan_sets += plus_dora(
                    tile,
                    enemy.table.dora_indicators,
                    add_aka_dora=enemy.table.has_aka_dora)

            tile_meld_34 = meld.tiles[0] // 4
            if tile_meld_34 in enemy.valued_honors:
                number_of_yakuhai_enemy_kan_sets += 1

        number_of_other_player_kan_sets = 0
        for other_player in enemy.table.players:
            if other_player.seat == enemy.seat:
                continue

            for meld in other_player.melds:
                if meld.type == MeldPrint.KAN or meld.type == MeldPrint.SHOUMINKAN:
                    number_of_other_player_kan_sets += 1

        tile_category = ""
        # additional danger for tiles that could be used for tanyao
        if not is_honor(tile_34):
            # +1 here to make it more readable
            simplified_tile = simplify(tile_34) + 1

            if simplified_tile in [4, 5, 6]:
                tile_category = "middle"

            if simplified_tile in [2, 3, 7, 8]:
                tile_category = "edge"

            if simplified_tile in [1, 9]:
                tile_category = "terminal"
        else:
            tile_category = "honor"
            if tile_34 in enemy.valued_honors:
                tile_category = "valuable_honor"

        return {
            "is_dealer":
            enemy.is_dealer and 1 or 0,
            "riichi_called_on_step":
            riichi_called_on_step,
            "current_enemy_step":
            len(enemy.discards),
            "wind_number":
            main_player.table.round_wind_number,
            "scores":
            enemy.scores,
            "is_tsumogiri_riichi":
            riichi_discard.is_tsumogiri and 1 or 0,
            "is_oikake_riichi":
            enemy.is_oikake_riichi and 1 or 0,
            "is_oikake_riichi_against_dealer_riichi_threat":
            enemy.is_oikake_riichi_against_dealer_riichi_threat and 1 or 0,
            "is_riichi_against_open_hand_threat":
            enemy.is_riichi_against_open_hand_threat and 1 or 0,
            "number_of_kan_in_enemy_hand":
            number_of_kan_in_enemy_hand,
            "number_of_dora_in_enemy_kan_sets":
            number_of_dora_in_enemy_kan_sets,
            "number_of_yakuhai_enemy_kan_sets":
            number_of_yakuhai_enemy_kan_sets,
            "number_of_other_player_kan_sets":
            number_of_other_player_kan_sets,
            "live_dora_tiles":
            live_dora_tiles,
            "tile_plus_dora":
            plus_dora(tile_136,
                      enemy.table.dora_indicators,
                      add_aka_dora=enemy.table.has_aka_dora),
            "tile_category":
            tile_category,
            "discards_before_riichi_34":
            ";".join([
                str(x.value // 4)
                for x in enemy.discards[:riichi_called_on_step]
            ]),
        }
Esempio n. 25
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)
Esempio n. 26
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)
Esempio n. 27
0
    def is_threatening(self) -> bool:
        """
        We are trying to determine other players current threat
        """
        round_step = len(self.enemy.discards)

        if self.enemy.in_riichi:
            self._create_danger_reason(EnemyDanger.THREAT_RIICHI,
                                       round_step=round_step)
            return True

        melds = self.enemy.melds
        # we can't analyze closed hands for now
        if not melds:
            return False

        active_yaku = []
        sure_han = 0

        yakuhai_analyzer = YakuhaiAnalyzer(self.enemy)
        if yakuhai_analyzer.is_yaku_active():
            active_yaku.append(yakuhai_analyzer)
            sure_han = yakuhai_analyzer.melds_han()

        yaku_analyzers = [
            ChinitsuAnalyzer(self.enemy),
            HonitsuAnalyzer(self.enemy),
            ToitoiAnalyzer(self.enemy),
            TanyaoAnalyzer(self.enemy),
        ]

        for x in yaku_analyzers:
            if x.is_yaku_active():
                active_yaku.append(x)

        if not active_yaku:
            active_yaku.append(AtodzukeAnalyzer(self.enemy))
            sure_han = 1

        # FIXME: probably our approach here should be refactored and we should not care about cost
        if not sure_han:
            main_yaku = [
                x for x in active_yaku if not x.is_absorbed(active_yaku)
            ]
            if main_yaku:
                sure_han = main_yaku[0].melds_han()
            else:
                sure_han = 1

        meld_tiles = self.enemy.meld_tiles
        dora_count = sum([
            plus_dora(x,
                      self.table.dora_indicators,
                      add_aka_dora=self.table.has_aka_dora) for x in meld_tiles
        ])
        sure_han += dora_count

        if len(melds) == 1 and round_step > 5 and sure_han >= 4:
            self._create_danger_reason(
                EnemyDanger.THREAT_OPEN_HAND_AND_MULTIPLE_DORA, melds,
                dora_count, active_yaku, round_step)
            return True

        if len(melds) >= 2 and round_step > 4 and sure_han >= 3:
            self._create_danger_reason(EnemyDanger.THREAT_EXPENSIVE_OPEN_HAND,
                                       melds, dora_count, active_yaku,
                                       round_step)
            return True

        if len(
                melds
        ) >= 1 and round_step > 10 and sure_han >= 2 and self.enemy.is_dealer:
            self._create_danger_reason(
                EnemyDanger.THREAT_OPEN_HAND_UNKNOWN_COST, melds, dora_count,
                active_yaku, round_step)
            return True

        # we are not sure how expensive this is, but let's be a little bit careful
        if (round_step > 14 and len(melds) >= 1) or (
                round_step > 9 and len(melds) >= 2) or len(melds) >= 3:
            self._create_danger_reason(
                EnemyDanger.THREAT_OPEN_HAND_UNKNOWN_COST, melds, dora_count,
                active_yaku, round_step)
            return True

        return False
Esempio n. 28
0
def prepare_closed_hand_input(
    round_wind,
    dora_indicators,
    player_closed_hand,
    player_melds,
    player_discards,
    tenpai_player_wind,
    tenpai_player_riichi,
    tenpai_player_melds,
    tenpai_player_discards,
    second_player_melds,
    second_player_discards,
    third_player_melds,
    third_player_discards,
):
    """
    meld dict format = {
        'tiles': List of 136 format tiles
    }

    discard dict format = {
        'tile': 136 format,
        'is_tsumogiri': bool,
        'is_after_meld': bool,
    }

    :param round_wind:                27 (east), 28 (south), 29 (west), 30 (north)
    :param dora_indicators:           List of 136 format tiles
    :param player_closed_hand:        Our player. List of 136 format tiles
    :param player_melds:              Our player. List of meld dicts
    :param player_discards:           Our player. List of discard dicts
    :param tenpai_player_wind:        27 (east), 28 (south), 29 (west), 30 (north)
    :param tenpai_player_riichi:      bool
    :param tenpai_player_melds:       Tenpai player. List of meld dicts
    :param tenpai_player_discards:    Tenpai player. List of discard dicts
    :param second_player_melds:       Second player. List of meld dicts
    :param second_player_discards:    Second player. List of discard dicts
    :param third_player_melds:        Third player. List of meld dicts
    :param third_player_discards:     Third player. List of discard dicts

    :return: list of 0 and 1 values
    """
    tiles_unique = 34

    max_dora_in_hand = 8
    max_dora_on_the_table = 16 + 3
    winds_input_size = 8

    tenpai_player_discards_input = [0 for _ in range(tiles_unique)]
    tenpai_player_melds_input = [1 for _ in range(tiles_unique)]
    winds_input = [0 for _ in range(winds_input_size)]
    dora_in_player_open_melds_input = [0 for _ in range(max_dora_in_hand)]
    not_visible_dora_on_the_table_input = [
        0 for _ in range(max_dora_on_the_table)
    ]

    for discard_dict in tenpai_player_discards:
        tile = discard_dict['tile'] // 4
        tenpai_player_discards_input[tile] = 1

    for meld in tenpai_player_melds:
        tiles = meld['tiles']
        for tile in tiles:
            tile = tile // 4
            tenpai_player_melds_input[tile] = 1

    out_tiles_136 = []

    for tile_136 in player_closed_hand:
        out_tiles_136.append(tile_136)

    for tile_136 in dora_indicators:
        out_tiles_136.append(tile_136)

    discards = [
        player_discards, tenpai_player_discards, second_player_discards,
        third_player_discards
    ]

    for discards_list in discards:
        for x in discards_list:
            # we will add this tile in melds loop
            if x['was_taken_for_meld']:
                continue

            out_tiles_136.append(x['tile'])

    melds = [
        player_melds, tenpai_player_melds, second_player_melds,
        third_player_melds
    ]

    for meld_list in melds:
        for x in meld_list:
            out_tiles_136.extend(x['tiles'])

    out_tiles = [0 for _ in range(tiles_unique)]
    for x in out_tiles_136:
        tile = x // 4

        out_tiles[tile] += 1
        assert out_tiles[tile] <= 4

    out_tiles_0 = [1 if x >= 1 else 0 for x in out_tiles]
    out_tiles_1 = [1 if x >= 2 else 0 for x in out_tiles]
    out_tiles_2 = [1 if x >= 3 else 0 for x in out_tiles]
    out_tiles_3 = [1 if x == 4 else 0 for x in out_tiles]

    if round_wind == EAST:
        winds_input[0] = 1
    elif round_wind == SOUTH:
        winds_input[1] = 1
    elif round_wind == WEST:
        winds_input[2] = 1
    elif round_wind == NORTH:
        winds_input[3] = 1

    if tenpai_player_wind == EAST:
        winds_input[4] = 1
    elif tenpai_player_wind == SOUTH:
        winds_input[5] = 1
    elif tenpai_player_wind == WEST:
        winds_input[6] = 1
    elif tenpai_player_wind == NORTH:
        winds_input[7] = 1

    number_of_dora_in_player_open_melds = 0
    player_melds = tenpai_player_melds
    for meld in player_melds:
        for tile in meld['tiles']:
            number_of_dora_in_player_open_melds += plus_dora(
                tile, dora_indicators)
            if is_aka_dora(tile, True):
                number_of_dora_in_player_open_melds += 1

    if number_of_dora_in_player_open_melds > max_dora_in_hand:
        number_of_dora_in_player_open_melds = max_dora_in_hand

    for i in range(max_dora_in_hand):
        if i + 1 <= number_of_dora_in_player_open_melds:
            dora_in_player_open_melds_input[i] = 1

    visible_dora = 0
    for visible_tile in out_tiles:
        visible_dora += plus_dora(visible_tile, dora_indicators)
        if is_aka_dora(visible_tile, True):
            visible_dora += 1

    not_visible_dora = max_dora_on_the_table - visible_dora

    for i in range(max_dora_on_the_table):
        if i + 1 <= not_visible_dora:
            not_visible_dora_on_the_table_input[i] = 1

    return list(
        itertools.chain(
            winds_input,
            not_visible_dora_on_the_table_input,
            dora_in_player_open_melds_input,
            tenpai_player_discards_input,
            tenpai_player_melds_input,
            out_tiles_0,
            out_tiles_1,
            out_tiles_2,
            out_tiles_3,
        ))
    def should_activate_strategy(self, tiles_136):
        """
        We can go for chinitsu strategy if we have prevalence of one suit
        """

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

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

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

        count_of_shuntsu_other_suits = 0
        count_of_koutsu_other_suits = 0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.chosen_suit = suit['function']

        return True
Esempio n. 30
0
 def is_dora(self, tile):
     return plus_dora(tile, self.dora_indicators) or is_aka_dora(tile, self.has_open_tanyao)
Esempio n. 31
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
Esempio n. 32
0
 def is_dora(self, tile):
     return plus_dora(tile, self.dora_indicators, add_aka_dora=self.has_aka_dora)
    def calculate_value(self):
        # base is 100 for ability to mark tiles as not needed (like set value to 50)
        value = 100
        honored_value = 20

        if is_honor(self.tile_to_discard):
            if self.tile_to_discard in self.player.valued_honors:
                count_of_winds = [x for x in self.player.valued_honors if x == self.tile_to_discard]
                # for west-west, east-east we had to double tile value
                value += honored_value * len(count_of_winds)
        else:
            # aim for tanyao
            if self.player.ai.current_strategy and self.player.ai.current_strategy.type == BaseStrategy.TANYAO:
                suit_tile_grades = [10, 20, 30, 50, 40, 50, 30, 20, 10]
            # usual hand
            else:
                suit_tile_grades = [10, 20, 40, 50, 30, 50, 40, 20, 10]

            simplified_tile = simplify(self.tile_to_discard)
            value += suit_tile_grades[simplified_tile]

            for indicator in self.player.table.dora_indicators:
                indicator_34 = indicator // 4
                if is_honor(indicator_34):
                    continue

                # indicator and tile not from the same suit
                if is_sou(indicator_34) and not is_sou(self.tile_to_discard):
                    continue

                # indicator and tile not from the same suit
                if is_man(indicator_34) and not is_man(self.tile_to_discard):
                    continue

                # indicator and tile not from the same suit
                if is_pin(indicator_34) and not is_pin(self.tile_to_discard):
                    continue

                simplified_indicator = simplify(indicator_34)
                simplified_dora = simplified_indicator + 1
                # indicator is 9 man
                if simplified_dora == 9:
                    simplified_dora = 0

                # tile so close to the dora
                if simplified_tile + 1 == simplified_dora or simplified_tile - 1 == simplified_dora:
                    value += DiscardOption.DORA_FIRST_NEIGHBOUR

                # tile not far away from dora
                if simplified_tile + 2 == simplified_dora or simplified_tile - 2 == simplified_dora:
                    value += DiscardOption.DORA_SECOND_NEIGHBOUR

        count_of_dora = plus_dora(self.tile_to_discard * 4, self.player.table.dora_indicators)

        tile_136 = self.find_tile_in_hand(self.player.closed_hand)
        if is_aka_dora(tile_136, self.player.table.has_aka_dora):
            count_of_dora += 1

        self.count_of_dora = count_of_dora
        value += count_of_dora * DiscardOption.DORA_VALUE

        if is_honor(self.tile_to_discard):
            # depends on how much honor tiles were discarded
            # we will decrease tile value
            discard_percentage = [100, 75, 20, 0, 0]
            discarded_tiles = self.player.table.revealed_tiles[self.tile_to_discard]

            value = (value * discard_percentage[discarded_tiles]) / 100

            # three honor tiles were discarded,
            # so we don't need this tile anymore
            if value == 0:
                self.had_to_be_discarded = True

        self.valuation = int(value)
Esempio n. 34
0
    def calculate_danger_borders(self, discard_options, threatening_player, all_threatening_players):
        min_shanten = min([x.shanten for x in discard_options])

        placement_adjustment = self.player.ai.placement.get_allowed_danger_modifier()
        for discard_option in discard_options:
            danger_border = DangerBorder.BETAORI
            hand_weighted_cost = 0
            tune = 0
            shanten = discard_option.shanten
            tile_136 = discard_option.tile_to_discard_136

            if discard_option.danger.get_total_danger_for_player(threatening_player.enemy.seat) == 0:
                threatening_player_hand_cost = 0
            else:
                threatening_player_hand_cost = threatening_player.get_assumed_hand_cost(
                    tile_136, discard_option.danger.can_be_used_for_ryanmen
                )

            # fast path: we don't need to calculate all the stuff if this tile is safe against this enemy
            if threatening_player_hand_cost == 0:
                discard_option.danger.set_danger_border(
                    threatening_player.enemy.seat, DangerBorder.IGNORE, hand_weighted_cost, threatening_player_hand_cost
                )
                continue

            if discard_option.shanten == 0:
                hand_weighted_cost = self.player.ai.estimate_weighted_mean_hand_value(discard_option)

                # we are not ready to push with hand that doesn't have chances to win
                # or to get ryukoku payments
                if hand_weighted_cost == 0:
                    discard_option.danger.set_danger_border(
                        threatening_player.enemy.seat,
                        DangerBorder.BETAORI,
                        hand_weighted_cost,
                        threatening_player_hand_cost,
                    )
                    continue

                discard_option.danger.weighted_cost = hand_weighted_cost
                cost_ratio = (hand_weighted_cost / threatening_player_hand_cost) * 100
                tune = self.player.config.TUNE_DANGER_BORDER_TEMPAI_VALUE

                if self.player.ai.placement.must_push(
                    all_threatening_players,
                    discard_option.tile_to_discard_136,
                    num_shanten=0,
                    tempai_cost=hand_weighted_cost,
                ):
                    danger_border = DangerBorder.IGNORE
                else:
                    # good wait
                    if discard_option.ukeire >= 6:
                        if cost_ratio >= 100:
                            danger_border = DangerBorder.IGNORE
                        elif cost_ratio >= 70:
                            danger_border = DangerBorder.VERY_HIGH
                        elif cost_ratio >= 50:
                            danger_border = DangerBorder.UPPER_MEDIUM
                        elif cost_ratio >= 30:
                            danger_border = DangerBorder.MEDIUM
                        else:
                            danger_border = DangerBorder.LOW
                    # moderate wait
                    elif discard_option.ukeire >= 4:
                        if cost_ratio >= 400:
                            danger_border = DangerBorder.IGNORE
                        elif cost_ratio >= 200:
                            danger_border = DangerBorder.EXTREME
                        elif cost_ratio >= 100:
                            danger_border = DangerBorder.VERY_HIGH
                        elif cost_ratio >= 70:
                            danger_border = DangerBorder.UPPER_MEDIUM
                        elif cost_ratio >= 50:
                            danger_border = DangerBorder.LOWER_MEDIUM
                        elif cost_ratio >= 30:
                            danger_border = DangerBorder.UPPER_LOW
                        else:
                            danger_border = DangerBorder.VERY_LOW
                    # weak wait
                    elif discard_option.ukeire >= 2:
                        if cost_ratio >= 400:
                            danger_border = DangerBorder.EXTREME
                        elif cost_ratio >= 200:
                            danger_border = DangerBorder.VERY_HIGH
                        elif cost_ratio >= 100:
                            danger_border = DangerBorder.UPPER_MEDIUM
                        elif cost_ratio >= 70:
                            danger_border = DangerBorder.MEDIUM
                        elif cost_ratio >= 50:
                            danger_border = DangerBorder.UPPER_LOW
                        elif cost_ratio >= 30:
                            danger_border = DangerBorder.LOW
                        else:
                            danger_border = DangerBorder.EXTREMELY_LOW
                    # waiting for 1 tile basically
                    else:
                        if cost_ratio >= 400:
                            danger_border = DangerBorder.HIGH
                        elif cost_ratio >= 200:
                            danger_border = DangerBorder.UPPER_MEDIUM
                        elif cost_ratio >= 100:
                            danger_border = DangerBorder.LOWER_MEDIUM
                        elif cost_ratio >= 50:
                            danger_border = DangerBorder.LOW
                        else:
                            danger_border = DangerBorder.EXTREMELY_LOW

            if discard_option.shanten == 1:
                tune = self.player.config.TUNE_DANGER_BORDER_1_SHANTEN_VALUE

                # FIXME: temporary solution to avoid too much ukeire2 calculation
                if min_shanten == 0:
                    hand_weighted_cost = 2000
                else:
                    hand_weighted_cost = discard_option.average_second_level_cost

                # never push with zero chance to win
                # FIXME: we may actually want to push it for tempai in ryukoku, so reconsider
                if not hand_weighted_cost:
                    discard_option.danger.set_danger_border(
                        threatening_player.enemy.seat,
                        DangerBorder.BETAORI,
                        hand_weighted_cost,
                        threatening_player_hand_cost,
                    )
                    continue

                discard_option.danger.weighted_cost = int(hand_weighted_cost)
                cost_ratio = (hand_weighted_cost / threatening_player_hand_cost) * 100
                average_tempai_waits = discard_option.average_second_level_waits

                if self.player.ai.placement.must_push(
                    all_threatening_players,
                    discard_option.tile_to_discard_136,
                    num_shanten=1,
                    tempai_cost=hand_weighted_cost,
                ):
                    danger_border = DangerBorder.IGNORE
                else:
                    # lots of ukeire
                    if discard_option.ukeire >= 32 and average_tempai_waits >= 6:
                        if cost_ratio >= 400:
                            danger_border = DangerBorder.IGNORE
                        elif cost_ratio >= 200:
                            danger_border = DangerBorder.EXTREME
                        elif cost_ratio >= 100:
                            danger_border = DangerBorder.VERY_HIGH
                        elif cost_ratio >= 50:
                            danger_border = DangerBorder.MEDIUM
                        elif cost_ratio >= 20:
                            danger_border = DangerBorder.UPPER_LOW
                        else:
                            danger_border = DangerBorder.EXTREMELY_LOW
                    # very good ukeire
                    elif discard_option.ukeire >= 20 and average_tempai_waits >= 6:
                        if cost_ratio >= 400:
                            danger_border = DangerBorder.IGNORE
                        elif cost_ratio >= 200:
                            danger_border = DangerBorder.EXTREME
                        elif cost_ratio >= 100:
                            danger_border = DangerBorder.VERY_HIGH
                        elif cost_ratio >= 50:
                            danger_border = DangerBorder.LOWER_MEDIUM
                        elif cost_ratio >= 20:
                            danger_border = DangerBorder.LOW
                        else:
                            danger_border = DangerBorder.EXTREMELY_LOW
                    # good ukeire
                    elif discard_option.ukeire >= 12 and average_tempai_waits >= 4:
                        if cost_ratio >= 400:
                            danger_border = DangerBorder.VERY_HIGH
                        elif cost_ratio >= 200:
                            danger_border = DangerBorder.HIGH
                        elif cost_ratio >= 100:
                            danger_border = DangerBorder.UPPER_MEDIUM
                        elif cost_ratio >= 50:
                            danger_border = DangerBorder.UPPER_LOW
                        elif cost_ratio >= 20:
                            danger_border = DangerBorder.VERY_LOW
                        else:
                            danger_border = DangerBorder.BETAORI
                    # mediocre ukeire
                    elif discard_option.ukeire >= 7 and average_tempai_waits >= 2:
                        if cost_ratio >= 400:
                            danger_border = DangerBorder.HIGH
                        elif cost_ratio >= 200:
                            danger_border = DangerBorder.UPPER_MEDIUM
                        elif cost_ratio >= 100:
                            danger_border = DangerBorder.LOWER_MEDIUM
                        elif cost_ratio >= 50:
                            danger_border = DangerBorder.VERY_LOW
                        elif cost_ratio >= 20:
                            danger_border = DangerBorder.LOWEST
                        else:
                            danger_border = DangerBorder.BETAORI
                    # very low ukeire
                    elif discard_option.ukeire >= 3 and average_tempai_waits >= 1:
                        if cost_ratio >= 400:
                            danger_border = DangerBorder.MEDIUM
                        elif cost_ratio >= 200:
                            danger_border = DangerBorder.UPPER_LOW
                        elif cost_ratio >= 100:
                            danger_border = DangerBorder.VERY_LOW
                        elif cost_ratio >= 50:
                            danger_border = DangerBorder.LOWEST
                        else:
                            danger_border = DangerBorder.BETAORI
                    # little to no ukeire
                    else:
                        danger_border = DangerBorder.BETAORI

            if discard_option.shanten == 2:
                tune = self.player.config.TUNE_DANGER_BORDER_2_SHANTEN_VALUE

                if self.player.is_dealer:
                    scale = [0, 1000, 2900, 5800, 7700, 12000, 18000, 18000, 24000, 24000, 48000]
                else:
                    scale = [0, 1000, 2000, 3900, 5200, 8000, 12000, 12000, 16000, 16000, 32000]

                if self.player.is_open_hand:
                    # FIXME: each strategy should have a han value, we should use it instead
                    han = 1
                else:
                    # TODO: try to estimate yaku chances for closed hand
                    han = 1

                dora_count = sum(
                    [
                        plus_dora(x, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora)
                        for x in self.player.tiles
                    ]
                )

                han += dora_count

                hand_weighted_cost = scale[min(han, len(scale) - 1)]

                discard_option.danger.weighted_cost = int(hand_weighted_cost)
                cost_ratio = (hand_weighted_cost / threatening_player_hand_cost) * 100

                if self.player.ai.placement.must_push(
                    all_threatening_players,
                    discard_option.tile_to_discard_136,
                    num_shanten=2,
                    tempai_cost=hand_weighted_cost,
                ):
                    danger_border = DangerBorder.IGNORE
                else:
                    # lots of ukeire
                    if discard_option.ukeire >= 40:
                        if cost_ratio >= 400:
                            danger_border = DangerBorder.HIGH
                        elif cost_ratio >= 200:
                            danger_border = DangerBorder.MEDIUM
                        elif cost_ratio >= 100:
                            danger_border = DangerBorder.EXTREMELY_LOW
                        else:
                            danger_border = DangerBorder.BETAORI
                    # very good ukeire
                    elif discard_option.ukeire >= 20:
                        if cost_ratio >= 400:
                            danger_border = DangerBorder.UPPER_MEDIUM
                        elif cost_ratio >= 200:
                            danger_border = DangerBorder.LOW
                        elif cost_ratio >= 100:
                            danger_border = DangerBorder.LOWEST
                        else:
                            danger_border = DangerBorder.BETAORI
                    # mediocre ukeire or worse
                    else:
                        danger_border = DangerBorder.BETAORI

            # if we could have chosen tempai, pushing 1 or more shanten is usually
            # a pretty bad idea, so tune down
            if discard_option.shanten != 0 and min_shanten == 0:
                danger_border = DangerBorder.tune_down(danger_border, 2)

            # depending on our placement we may want to be more defensive or more offensive
            tune += placement_adjustment
            danger_border = DangerBorder.tune(danger_border, tune)

            # if it's late there are generally less reasons to be aggressive
            danger_border = DangerBorder.tune_for_round(self.player, danger_border, shanten)

            discard_option.danger.set_danger_border(
                threatening_player.enemy.seat, danger_border, hand_weighted_cost, threatening_player_hand_cost
            )
        return discard_options
Esempio n. 35
0
    def should_activate_strategy(self, tiles_136, meld_tile=None):
        """
        We can go for chinitsu strategy if we have prevalence of one suit
        """

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

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

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

        count_of_shuntsu_other_suits = 0
        count_of_koutsu_other_suits = 0

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

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

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

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

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

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

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

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

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

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

        # only 11 tiles or less by 12th turn is too slow, considering alternative
        if suit["count"] <= 11 and self.player.round_step > 11:
            return False

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

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

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

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

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

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

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

        self.chosen_suit = suit["function"]

        return True
Esempio n. 36
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
Esempio n. 37
0
    def _calculate_assumed_hand_cost_for_riichi(
            self, tile_136, can_be_used_for_ryanmen) -> int:
        scale_index = 0
        tile_34 = tile_136 // 4

        if self.enemy.is_dealer:
            scale = [
                2900, 5800, 7700, 12000, 12000, 18000, 18000, 24000, 24000,
                48000
            ]
        else:
            scale = [
                2000, 3900, 5200, 8000, 8000, 12000, 12000, 16000, 16000, 32000
            ]

        # it wasn't early riichi, let's think that it could be more expensive
        if 6 <= self.enemy.riichi_called_on_step <= 11:
            scale_index += 1

        # more late riichi, probably means more expensive riichi
        if self.enemy.riichi_called_on_step >= 12:
            scale_index += 2

        if self.enemy.is_ippatsu:
            scale_index += 1

        total_dora_in_game = len(self.table.dora_indicators) * 4 + (
            3 * int(self.table.has_aka_dora))
        visible_tiles = self.table.revealed_tiles_136 + self.main_player.closed_hand
        visible_dora_tiles = sum([
            plus_dora(x,
                      self.table.dora_indicators,
                      add_aka_dora=self.table.has_aka_dora)
            for x in visible_tiles
        ])
        live_dora_tiles = total_dora_in_game - visible_dora_tiles
        assert live_dora_tiles >= 0, "Live dora tiles can't be less than 0"
        # there are too many live dora tiles, let's increase hand cost
        if live_dora_tiles >= 4:
            scale_index += 1

        # if we are discarding dora we are obviously going to make enemy hand more expensive
        scale_index += self._get_dora_scale_bonus(tile_136)

        # if enemy has closed kan, his hand is more expensive on average
        for meld in self.enemy.melds:
            # if he is in riichi he can only have closed kan
            assert meld.type == Meld.KAN and not meld.opened

            # plus two just because of riichi with kan
            scale_index += 2

            # higher danger for doras
            for tile in meld.tiles:
                scale_index += plus_dora(tile,
                                         self.table.dora_indicators,
                                         add_aka_dora=self.table.has_aka_dora)

            # higher danger for yakuhai
            tile_meld_34 = meld.tiles[0] // 4
            scale_index += len(
                [x for x in self.enemy.valued_honors if x == tile_meld_34])

        # let's add more danger for all other opened kan sets on the table
        for other_player in self.table.players:
            if other_player.seat == self.enemy.seat:
                continue

            for meld in other_player.melds:
                if meld.type == Meld.KAN or meld.type == Meld.SHOUMINKAN:
                    scale_index += 1

        # additional danger for tiles that could be used for tanyao
        if not is_honor(tile_34):
            # +1 here to make it more readable
            simplified_tile = simplify(tile_34) + 1

            if simplified_tile in [4, 5, 6]:
                scale_index += 1

            if simplified_tile in [2, 3, 7, 8] and can_be_used_for_ryanmen:
                scale_index += 1

        if scale_index > len(scale) - 1:
            scale_index = len(scale) - 1

        return scale[scale_index]
Esempio n. 38
0
    def calculate_valuation(self):
        # base is 100 for ability to mark tiles as not needed (like set value to 50)
        value = 100
        honored_value = 20

        if is_honor(self.tile_to_discard_34):
            if self.tile_to_discard_34 in self.player.valued_honors:
                count_of_winds = [
                    x for x in self.player.valued_honors
                    if x == self.tile_to_discard_34
                ]
                # for west-west, east-east we had to double tile value
                value += honored_value * len(count_of_winds)
        else:
            # aim for tanyao
            if self.player.ai.current_strategy and self.player.ai.current_strategy.type == BaseStrategy.TANYAO:
                suit_tile_grades = [10, 20, 30, 50, 40, 50, 30, 20, 10]
            # usual hand
            else:
                suit_tile_grades = [10, 20, 40, 50, 30, 50, 40, 20, 10]

            simplified_tile = simplify(self.tile_to_discard_34)
            value += suit_tile_grades[simplified_tile]

            for indicator in self.player.table.dora_indicators:
                indicator_34 = indicator // 4
                if is_honor(indicator_34):
                    continue

                # indicator and tile not from the same suit
                if is_sou(indicator_34) and not is_sou(
                        self.tile_to_discard_34):
                    continue

                # indicator and tile not from the same suit
                if is_man(indicator_34) and not is_man(
                        self.tile_to_discard_34):
                    continue

                # indicator and tile not from the same suit
                if is_pin(indicator_34) and not is_pin(
                        self.tile_to_discard_34):
                    continue

                simplified_indicator = simplify(indicator_34)
                simplified_dora = simplified_indicator + 1
                # indicator is 9 man
                if simplified_dora == 9:
                    simplified_dora = 0

                # tile so close to the dora
                if simplified_tile + 1 == simplified_dora or simplified_tile - 1 == simplified_dora:
                    value += DiscardOption.DORA_FIRST_NEIGHBOUR

                # tile not far away from dora
                if simplified_tile + 2 == simplified_dora or simplified_tile - 2 == simplified_dora:
                    value += DiscardOption.DORA_SECOND_NEIGHBOUR

        count_of_dora = plus_dora(self.tile_to_discard_136,
                                  self.player.table.dora_indicators,
                                  add_aka_dora=self.player.table.has_aka_dora)

        self.count_of_dora = count_of_dora
        value += count_of_dora * DiscardOption.DORA_VALUE

        if is_honor(self.tile_to_discard_34):
            # depends on how much honor tiles were discarded
            # we will decrease tile value
            discard_percentage = [100, 75, 20, 0, 0]
            discarded_tiles = self.player.table.revealed_tiles[
                self.tile_to_discard_34]

            value = (value * discard_percentage[discarded_tiles]) / 100

            # three honor tiles were discarded,
            # so we don't need this tile anymore
            if value == 0:
                self.had_to_be_discarded = True

        self.valuation = int(value)
Esempio n. 39
0
    def should_go_to_defence_mode(self, discard_candidate=None):
        """
        The method is decides should bot go to the defence mode or not.
        For now only full defence is possible
        :return: true|false
        """

        # we drew a tile, so we have 14 tiles in our hand
        if discard_candidate:
            shanten = discard_candidate.shanten
            waiting = discard_candidate.waiting
        # we have 13 tiles in hand (this is not our turn)
        else:
            shanten = self.player.ai.shanten
            waiting = self.player.ai.waiting

        if not waiting:
            waiting = []

        # if we are in riichi, we can't defence
        if self.player.in_riichi:
            return False

        threatening_players = self._get_threatening_players()

        # no one is threatening, so we can build our hand
        if len(threatening_players) == 0:
            return False

        if shanten == 1:
            # TODO calculate all possible hand costs for 1-2 shanten
            dora_count = sum([plus_dora(x, self.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.table.has_open_tanyao)])
            # we had 3+ dora in our almost done hand,
            # we can try to push it
            if dora_count >= 3:
                return False

        # our hand is not tempai, so better to fold it
        if shanten != 0:
            return True

        # we are in tempai, let's try to estimate hand value
        hands_estimated_cost = []
        call_riichi = not self.player.is_open_hand
        for tile in waiting:
            # copy of tiles, because we are modifying a list
            tiles = self.player.tiles[:]

            # special case, when we already have 14 tiles in the hand
            if discard_candidate:
                temp_tile = discard_candidate.find_tile_in_hand(self.player.closed_hand)
                tiles.remove(temp_tile)

            hand_result = self.player.ai.estimate_hand_value(tile, tiles, call_riichi)
            if hand_result.error is None:
                hands_estimated_cost.append(hand_result.cost['main'])

        # probably we are with opened hand without yaku, let's fold it
        if not hands_estimated_cost:
            return True

        max_cost = max(hands_estimated_cost)
        # our open hand in tempai, but it is cheap
        # so we can fold it
        if self.player.is_open_hand and max_cost < 7000:
            return True

        # when we call riichi we can get ura dora,
        # so it is reasonable to riichi 3k+ hands
        if not self.player.is_open_hand:
            # there are a lot of chances that we will not win with a bad wait
            # against other threatening players
            if max_cost < 3000 or len(waiting) < 2:
                return True

        return False