Пример #1
0
    def is_sanshoku_douko(self, hand):
        """
        Three pon sets consisting of the same numbers in all three suits
        :param hand: list of hand's sets
        :return: true|false
        """
        pon_sets = [i for i in hand if is_pon(i)]
        if len(pon_sets) < 3:
            return False

        sou_pon = []
        pin_pon = []
        man_pon = []
        for item in pon_sets:
            if is_sou(item[0]):
                sou_pon.append(item)
            elif is_pin(item[0]):
                pin_pon.append(item)
            elif is_man(item[0]):
                man_pon.append(item)

        for sou_item in sou_pon:
            for pin_item in pin_pon:
                for man_item in man_pon:
                    # cast tile indices to 1..9 representation
                    sou_item = [simplify(x) for x in sou_item]
                    pin_item = [simplify(x) for x in pin_item]
                    man_item = [simplify(x) for x in man_item]
                    if sou_item == pin_item == man_item:
                        return True
        return False
Пример #2
0
    def is_condition_met(self, hand, *args):
        chi_sets = [i for i in hand if is_chi(i)]
        if len(chi_sets) < 3:
            return False

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

        for sou_item in sou_chi:
            for pin_item in pin_chi:
                for man_item in man_chi:
                    # cast tile indices to 0..8 representation
                    sou_item = [simplify(x) for x in sou_item]
                    pin_item = [simplify(x) for x in pin_item]
                    man_item = [simplify(x) for x in man_item]
                    if sou_item == pin_item == man_item:
                        return True
        return False
Пример #3
0
    def is_sanshoku(self, hand):
        """
        The same chi in three suits
        :param hand: list of hand's sets
        :return: true|false
        """
        chi_sets = [i for i in hand if is_chi(i)]
        if len(chi_sets) < 3:
            return False

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

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

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

        sets = [sou_chi, pin_chi, man_chi]

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

            casted_sets = []

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

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

        return False
Пример #5
0
    def is_condition_met(self, hand, *args):
        pon_sets = [i for i in hand if is_pon_or_kan(i)]
        if len(pon_sets) < 3:
            return False

        sou_pon = []
        pin_pon = []
        man_pon = []
        for item in pon_sets:
            if is_sou(item[0]):
                sou_pon.append(item)
            elif is_pin(item[0]):
                pin_pon.append(item)
            elif is_man(item[0]):
                man_pon.append(item)

        for sou_item in sou_pon:
            for pin_item in pin_pon:
                for man_item in man_pon:
                    # cast tile indices to 1..9 representation
                    sou_item = set([simplify(x) for x in sou_item])
                    pin_item = set([simplify(x) for x in pin_item])
                    man_item = set([simplify(x) for x in man_item])
                    if sou_item == pin_item == man_item:
                        return True
        return False
Пример #6
0
    def _is_matagi_suji(self, enemy_analyzer, tile_analyze_34):
        discards = enemy_analyzer.enemy.discards
        discards_34 = [x.value // 4 for x in enemy_analyzer.enemy.discards]

        # too early to check matagi suji
        if len(discards) <= 5:
            return False
        # on middle stage check matagi pattern only for one latest discard
        elif len(discards) <= 9:
            latest_discards = [x for x in discards if not x.is_tsumogiri][-1:]
        else:
            # on late stage check matagi pattern for two latest discards
            latest_discards = [x for x in discards if not x.is_tsumogiri][-2:]

        latest_discards_34 = [x.value // 4 for x in latest_discards]
        # make sure that these discards are unique
        latest_discards_34 = list(set(latest_discards_34))

        matagi_patterns_config = [
            {"tile": 2, "dangers": [[1, 4]]},
            {"tile": 3, "dangers": [[1, 4], [2, 5]]},
            {"tile": 4, "dangers": [[2, 5], [3, 6]]},
            {"tile": 5, "dangers": [[3, 6], [4, 7]]},
            {"tile": 6, "dangers": [[4, 7], [5, 8]]},
            {"tile": 7, "dangers": [[5, 8], [6, 9]]},
            {"tile": 8, "dangers": [[6, 9]]},
        ]

        for enemy_discard_34 in latest_discards_34:
            if not is_tiles_same_suit(enemy_discard_34, tile_analyze_34):
                continue

            # +1 here to make it easier read matagi patterns
            enemy_discard_simplified = simplify(enemy_discard_34) + 1
            tile_analyze_simplified = simplify(tile_analyze_34) + 1

            for matagi_pattern_config in matagi_patterns_config:
                if matagi_pattern_config["tile"] != enemy_discard_simplified:
                    continue

                same_suit_simple_discards = []
                for is_suit in [is_pin, is_sou, is_man]:
                    if not is_suit(tile_analyze_34):
                        continue

                    same_suit_simple_discards = []
                    for discard_34 in discards_34:
                        if is_suit(discard_34):
                            # +1 here to make it easier to read
                            same_suit_simple_discards.append(simplify(discard_34) + 1)

                for danger in matagi_pattern_config["dangers"]:

                    has_suji_in_discard = len(list(set(same_suit_simple_discards) & set(danger))) != 0
                    if not has_suji_in_discard and tile_analyze_simplified in danger:
                        return True

        return False
Пример #7
0
    def is_aidayonken_pattern(self, enemy_analyzer, tile_analyze_34):
        discards = enemy_analyzer.enemy_discards_until_all_tsumogiri
        discards_34 = [x.value // 4 for x in discards]

        patterns_config = [
            {
                "pattern": [1, 6],
                "danger": [2, 5],
            },
            {
                "pattern": [2, 7],
                "danger": [3, 6],
            },
            {
                "pattern": [3, 8],
                "danger": [4, 7],
            },
            {
                "pattern": [4, 9],
                "danger": [5, 8],
            },
        ]

        for is_suit in [is_pin, is_sou, is_man]:
            if not is_suit(tile_analyze_34):
                continue

            same_suit_simple_discards = []
            for discard_34 in discards_34:
                if is_suit(discard_34):
                    # +1 here to make it easier to read
                    same_suit_simple_discards.append(simplify(discard_34) + 1)

            # +1 here to make it easier to read
            tile_analyze_simplified = simplify(tile_analyze_34) + 1

            for pattern_config in patterns_config:
                has_pattern = (list(
                    set(same_suit_simple_discards)
                    & set(pattern_config["pattern"])) ==
                               pattern_config["pattern"])
                if not has_pattern:
                    continue

                has_suji_in_discard = len(
                    list(
                        set(same_suit_simple_discards)
                        & set(pattern_config["danger"]))) != 0
                # we found aidayonken pattern in the discard
                # and aidayonken danger tiles are not in the discard
                # in that case we can increase danger for them
                if not has_suji_in_discard and tile_analyze_simplified in pattern_config[
                        "danger"]:
                    return True

        return False
Пример #8
0
    def _process_danger_for_2_8_tiles_suji_and_kabe(
        self, enemy_analyzer, tile_34, number_of_revealed_tiles, suji_tiles, kabe_tiles
    ):
        have_strong_kabe = [x for x in kabe_tiles if tile_34 == x["tile"] and x["type"] == Kabe.STRONG_KABE]
        if have_strong_kabe:
            if enemy_analyzer.enemy.is_open_hand:
                if number_of_revealed_tiles == 1:
                    return TileDanger.SHONPAI_KABE_STRONG_OPEN_HAND
                else:
                    return TileDanger.NON_SHONPAI_KABE_STRONG_OPEN_HAND
            else:
                if number_of_revealed_tiles == 1:
                    return TileDanger.SHONPAI_KABE_STRONG
                else:
                    return TileDanger.NON_SHONPAI_KABE_STRONG

        have_weak_kabe = [x for x in kabe_tiles if tile_34 == x["tile"] and x["type"] == Kabe.WEAK_KABE]
        if have_weak_kabe:
            if enemy_analyzer.enemy.is_open_hand:
                if number_of_revealed_tiles == 1:
                    return TileDanger.SHONPAI_KABE_WEAK_OPEN_HAND
                else:
                    return TileDanger.NON_SHONPAI_KABE_WEAK_OPEN_HAND
            else:
                if number_of_revealed_tiles == 1:
                    return TileDanger.SHONPAI_KABE_WEAK
                else:
                    return TileDanger.NON_SHONPAI_KABE_WEAK

        # only consider suji if there is no kabe
        have_suji = [x for x in suji_tiles if tile_34 == x]
        if have_suji:
            if enemy_analyzer.enemy.riichi_tile_136 is not None:
                enemy_riichi_tile_34 = enemy_analyzer.enemy.riichi_tile_136 // 4
                riichi_on_suji = [x for x in suji_tiles if enemy_riichi_tile_34 == x]

                # if it's 2378, then check if riichi was on suji tile
                if simplify(enemy_riichi_tile_34) == 4 and (simplify(tile_34) == 1 or simplify(tile_34) == 7):
                    return TileDanger.SUJI_28_ON_RIICHI

                if simplify(tile_34) == 2 or simplify(tile_34) == 6:
                    if 3 <= simplify(enemy_riichi_tile_34) <= 5 and riichi_on_suji:
                        return TileDanger.SUJI_37_ON_RIICHI
            elif enemy_analyzer.enemy.is_open_hand:
                return TileDanger.SUJI_OPEN_HAND

            return TileDanger.SUJI

        return None
Пример #9
0
    def _count_of_shuntsu(tiles, suit):
        suit_tiles = []
        for x in range(0, 34):
            tile = tiles[x]
            if not tile:
                continue

            if suit(x):
                suit_tiles.append(x)

        count_of_left_tiles = 0
        count_of_middle_tiles = 0
        count_of_right_tiles = 0

        simple_tiles = [simplify(x) for x in suit_tiles]
        for x in range(0, len(simple_tiles)):
            tile = simple_tiles[x]

            if tile + 1 in simple_tiles and tile + 2 in simple_tiles:
                count_of_left_tiles += 1

            if tile - 1 in simple_tiles and tile + 1 in simple_tiles:
                count_of_middle_tiles += 1

            if tile - 2 in simple_tiles and tile - 1 in simple_tiles:
                count_of_right_tiles += 1

        return (count_of_left_tiles + count_of_middle_tiles + count_of_right_tiles) // 3
Пример #10
0
    def _find_ryanmen_waits(tiles, suit):
        suit_tiles = []
        for x in range(0, 34):
            tile = tiles[x]
            if not tile:
                continue

            if suit(x):
                suit_tiles.append(x)

        count_of_ryanmen_waits = 0
        simple_tiles = [simplify(x) for x in suit_tiles]
        for x in range(0, len(simple_tiles)):
            tile = simple_tiles[x]
            # we cant build ryanmen with 1 and 9
            if tile == 1 or tile == 9:
                continue

            # bordered tile
            if x + 1 == len(simple_tiles):
                continue

            if tile + 1 == simple_tiles[x + 1]:
                count_of_ryanmen_waits += 1

        return count_of_ryanmen_waits
Пример #11
0
    def _count_of_shuntsu(tiles, suit):
        suit_tiles = []
        for x in range(0, 34):
            tile = tiles[x]
            if not tile:
                continue

            if suit(x):
                suit_tiles.append(x)

        count_of_left_tiles = 0
        count_of_middle_tiles = 0
        count_of_right_tiles = 0

        simple_tiles = [simplify(x) for x in suit_tiles]
        for x in range(0, len(simple_tiles)):
            tile = simple_tiles[x]

            if tile + 1 in simple_tiles and tile + 2 in simple_tiles:
                count_of_left_tiles += 1

            if tile - 1 in simple_tiles and tile + 1 in simple_tiles:
                count_of_middle_tiles += 1

            if tile - 2 in simple_tiles and tile - 1 in simple_tiles:
                count_of_right_tiles += 1

        return (count_of_left_tiles + count_of_middle_tiles + count_of_right_tiles) // 3
Пример #12
0
    def _find_ryanmen_waits(self, tiles, suit):
        suit_tiles = []
        for x in range(0, 34):
            tile = tiles[x]
            if not tile:
                continue

            if suit(x):
                suit_tiles.append(x)

        count_of_ryanmen_waits = 0
        simple_tiles = [simplify(x) for x in suit_tiles]
        for x in range(0, len(simple_tiles)):
            tile = simple_tiles[x]
            # we cant build ryanmen with 1 and 9
            if tile == 1 or tile == 9:
                continue

            # bordered tile
            if x + 1 == len(simple_tiles):
                continue

            if tile + 1 == simple_tiles[x + 1]:
                count_of_ryanmen_waits += 1

        return count_of_ryanmen_waits
Пример #13
0
    def is_condition_met(self, hand, allow_other_sets, *args):
        sou_sets = 0
        pin_sets = 0
        man_sets = 0
        honor_sets = 0
        for item in hand:
            if is_sou(item[0]):
                sou_sets += 1
            elif is_pin(item[0]):
                pin_sets += 1
            elif is_man(item[0]):
                man_sets += 1
            else:
                honor_sets += 1

        sets = [sou_sets, pin_sets, man_sets]
        only_one_suit = len([x for x in sets if x != 0]) == 1
        if not only_one_suit or honor_sets > 0:
            return False

        if not allow_other_sets and pin_sets == 0:
            # if we are not allowing other sets than pins
            return False

        indices = reduce(lambda z, y: z + y, hand)
        # cast tile indices to 0..8 representation
        indices = [simplify(x) for x in indices]

        # check for pairs
        for x in range(1, 8):
            if len([y for y in indices if y == x]) != 2:
                return False

        return True
Пример #14
0
    def _suits_tiles(self, tiles_34):
        """
        Return tiles separated by suits
        :param tiles_34:
        :return:
        """
        suits = [
            [0] * 9,
            [0] * 9,
            [0] * 9,
        ]

        for tile in range(0, EAST):
            total_tiles = self.player.total_tiles(tile, tiles_34)
            if not total_tiles:
                continue

            suit_index = None
            simplified_tile = simplify(tile)

            if is_man(tile):
                suit_index = 0

            if is_pin(tile):
                suit_index = 1

            if is_sou(tile):
                suit_index = 2

            suits[suit_index][simplified_tile] += total_tiles

        return suits
Пример #15
0
    def find_suji(self, safe_tiles_34):
        suji = []
        suits = [[], [], []]

        # let's cast each tile to 0-8 presentation
        for tile in safe_tiles_34:
            if is_man(tile):
                suits[0].append(simplify(tile))

            if is_pin(tile):
                suits[1].append(simplify(tile))

            if is_sou(tile):
                suits[2].append(simplify(tile))

        for x in range(0, 3):
            simplified_tiles = suits[x]
            base = x * 9

            # 1-4-7
            if 3 in simplified_tiles:
                suji.append(self.FIRST_SUJI + base)

            # double 1-4-7
            if 0 in simplified_tiles and 6 in simplified_tiles:
                suji.append(self.FIRST_SUJI + base)

            # 2-5-8
            if 4 in simplified_tiles:
                suji.append(self.SECOND_SUJI + base)

            # double 2-5-8
            if 1 in simplified_tiles and 7 in simplified_tiles:
                suji.append(self.SECOND_SUJI + base)

            # 3-6-9
            if 5 in simplified_tiles:
                suji.append(self.THIRD_SUJI + base)

            # double 3-6-9
            if 2 in simplified_tiles and 8 in simplified_tiles:
                suji.append(self.THIRD_SUJI + base)

        suji = list(set(suji))

        return suji
Пример #16
0
    def find_suji(self, safe_tiles_34):
        suji = []
        suits = [[], [], []]

        # let's cast each tile to 0-8 presentation
        for tile in safe_tiles_34:
            if is_man(tile):
                suits[0].append(simplify(tile))

            if is_pin(tile):
                suits[1].append(simplify(tile))

            if is_sou(tile):
                suits[2].append(simplify(tile))

        for x in range(0, 3):
            simplified_tiles = suits[x]
            base = x * 9

            # 1-4-7
            if 3 in simplified_tiles:
                suji.append(self.FIRST_SUJI + base)

            # double 1-4-7
            if 0 in simplified_tiles and 6 in simplified_tiles:
                suji.append(self.FIRST_SUJI + base)

            # 2-5-8
            if 4 in simplified_tiles:
                suji.append(self.SECOND_SUJI + base)

            # double 2-5-8
            if 1 in simplified_tiles and 7 in simplified_tiles:
                suji.append(self.SECOND_SUJI + base)

            # 3-6-9
            if 5 in simplified_tiles:
                suji.append(self.THIRD_SUJI + base)

            # double 3-6-9
            if 2 in simplified_tiles and 8 in simplified_tiles:
                suji.append(self.THIRD_SUJI + base)

        suji = list(set(suji))

        return suji
Пример #17
0
    def is_ittsu(self, hand):
        """
        Three sets of same suit: 1-2-3, 4-5-6, 7-8-9
        :param hand: list of hand's sets
        :return: true|false
        """

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

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

        sets = [sou_chi, pin_chi, man_chi]

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

            casted_sets = []

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

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

        return False
Пример #18
0
    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
Пример #19
0
    def is_chuuren_poutou(self, hand):
        """
        The hand contains 1-1-1-2-3-4-5-6-7-8-9-9-9 of one suit, plus any other tile of the same suit.
        :param hand: list of hand's sets
        :return: true|false
        """

        sou_sets = 0
        pin_sets = 0
        man_sets = 0
        honor_sets = 0
        for item in hand:
            if is_sou(item[0]):
                sou_sets += 1
            elif is_pin(item[0]):
                pin_sets += 1
            elif is_man(item[0]):
                man_sets += 1
            else:
                honor_sets += 1

        sets = [sou_sets, pin_sets, man_sets]
        only_one_suit = len([x for x in sets if x != 0]) == 1
        if not only_one_suit or honor_sets > 0:
            return False

        indices = reduce(lambda z, y: z + y, hand)
        # cast tile indices to 0..8 representation
        indices = [simplify(x) for x in indices]

        # 1-1-1
        if len([x for x in indices if x == 0]) < 3:
            return False

        # 9-9-9
        if len([x for x in indices if x == 8]) < 3:
            return False

        # 1-2-3-4-5-6-7-8-9 and one tile to any of them
        indices.remove(0)
        indices.remove(0)
        indices.remove(8)
        indices.remove(8)
        for x in range(0, 9):
            if x in indices:
                indices.remove(x)

        if len(indices) == 1:
            return True

        return False
Пример #20
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
Пример #21
0
    def is_condition_met(self, hand, *args):
        sou_sets = 0
        pin_sets = 0
        man_sets = 0
        honor_sets = 0
        for item in hand:
            if is_sou(item[0]):
                sou_sets += 1
            elif is_pin(item[0]):
                pin_sets += 1
            elif is_man(item[0]):
                man_sets += 1
            else:
                honor_sets += 1

        sets = [sou_sets, pin_sets, man_sets]
        only_one_suit = len([x for x in sets if x != 0]) == 1
        if not only_one_suit or honor_sets > 0:
            return False

        indices = reduce(lambda z, y: z + y, hand)
        # cast tile indices to 0..8 representation
        indices = [simplify(x) for x in indices]

        # 1-1-1
        if len([x for x in indices if x == 0]) < 3:
            return False

        # 9-9-9
        if len([x for x in indices if x == 8]) < 3:
            return False

        # 1-2-3-4-5-6-7-8-9 and one tile to any of them
        indices.remove(0)
        indices.remove(0)
        indices.remove(8)
        indices.remove(8)
        for x in range(0, 9):
            if x in indices:
                indices.remove(x)

        if len(indices) == 1:
            return True

        return False
Пример #22
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
Пример #23
0
    def find_tiles_to_discard(self, players):
        found_suji = []
        for player in players:
            suji = []
            suits = [[], [], []]

            # let's cast each tile to 0-8 presentation
            safe_tiles = player.all_safe_tiles
            for tile in safe_tiles:
                if is_man(tile):
                    suits[0].append(simplify(tile))

                if is_pin(tile):
                    suits[1].append(simplify(tile))

                if is_sou(tile):
                    suits[2].append(simplify(tile))

            for x in range(0, 3):
                simplified_tiles = suits[x]
                base = x * 9

                # 1-4-7
                if 3 in simplified_tiles:
                    suji.append(self.FIRST_SUJI + base)

                # double 1-4-7
                if 0 in simplified_tiles and 6 in simplified_tiles:
                    suji.append(self.FIRST_SUJI + base)

                # 2-5-8
                if 4 in simplified_tiles:
                    suji.append(self.SECOND_SUJI + base)

                # double 2-5-8
                if 1 in simplified_tiles and 7 in simplified_tiles:
                    suji.append(self.SECOND_SUJI + base)

                # 3-6-9
                if 5 in simplified_tiles:
                    suji.append(self.THIRD_SUJI + base)

                # double 3-6-9
                if 2 in simplified_tiles and 8 in simplified_tiles:
                    suji.append(self.THIRD_SUJI + base)

            suji = list(set(suji))

            found_suji.append(suji)

        if not found_suji:
            return []

        common_suji = list(set.intersection(*map(set, found_suji)))

        tiles = []
        for suji in common_suji:
            if not suji:
                continue

            tiles.extend(self._suji_tiles(suji))

        return tiles
Пример #24
0
    def _get_early_danger_bonus(self, enemy_analyzer, tile_analyze_34, has_other_danger_bonus):
        discards = enemy_analyzer.enemy_discards_until_all_tsumogiri
        discards_34 = [x.value // 4 for x in discards]

        assert not is_honor(tile_analyze_34)
        # +1 here to make it easier to read
        tile_analyze_simplified = simplify(tile_analyze_34) + 1
        # we only those border tiles
        if tile_analyze_simplified not in [1, 2, 8, 9]:
            return None

        # too early to make statements
        if len(discards_34) <= 5:
            return None

        central_discards_34 = [x for x in discards_34 if not is_honor(x) and not is_terminal(x)]
        # also too early to make statements
        if len(central_discards_34) <= 3:
            return None

        # we also want to check how many non-tsumogiri tiles there were after those discards
        latest_discards_34 = [x.value // 4 for x in discards if not x.is_tsumogiri][-3:]
        if len(latest_discards_34) != 3:
            return None

        # no more than 3, but we expect at least 3 non-central tiles after that one for pattern to matter
        num_early_discards = min(len(central_discards_34) - 3, 3)
        first_central_discards_34 = central_discards_34[:num_early_discards]

        patterns_config = []
        if not has_other_danger_bonus:
            # patterns lowering danger has higher priority in case they are possible
            # +1 implied here to make it easier to read
            # order is important, as 28 priority pattern is higher than 37 one
            patterns_config.extend(
                [
                    {
                        "pattern": 2,
                        "danger": [1],
                        "bonus": TileDanger.BONUS_EARLY_28,
                    },
                    {
                        "pattern": 8,
                        "danger": [9],
                        "bonus": TileDanger.BONUS_EARLY_28,
                    },
                    {
                        "pattern": 3,
                        "danger": [1, 2],
                        "bonus": TileDanger.BONUS_EARLY_37,
                    },
                    {
                        "pattern": 7,
                        "danger": [8, 9],
                        "bonus": TileDanger.BONUS_EARLY_37,
                    },
                ]
            )
        # patterns increasing danger have lower priority, but are always applied
        patterns_config.extend(
            [
                {
                    "pattern": 5,
                    "danger": [1, 9],
                    "bonus": TileDanger.BONUS_EARLY_5,
                },
            ]
        )

        # we return the first pattern we see
        for enemy_discard_34 in first_central_discards_34:
            # being also discarded late from hand kinda ruins our previous logic, so don't modify danger in that case
            if enemy_discard_34 in latest_discards_34:
                continue

            if not is_tiles_same_suit(enemy_discard_34, tile_analyze_34):
                continue

            # +1 here to make it easier read matagi patterns
            enemy_discard_simplified = simplify(enemy_discard_34) + 1
            for pattern_config in patterns_config:
                has_pattern = enemy_discard_simplified == pattern_config["pattern"]
                if not has_pattern:
                    continue

                if tile_analyze_simplified in pattern_config["danger"]:
                    return pattern_config["bonus"]

        return None
Пример #25
0
    def calculate_additional_fu(self, win_tile, hand, is_tsumo, player_wind,
                                round_wind, open_sets, called_kan_indices):
        """
        :param win_tile: "136 format" tile
        :param hand: list of hand's sets
        :param player_wind:
        :param round_wind:
        :param open_sets: array of array with 34 tiles format
        :param called_kan_indices: array of 34 tiles format
        :return: int
        """
        win_tile //= 4
        additional_fu = 0

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

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

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

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

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

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

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

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

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

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

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

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

        return additional_fu
Пример #26
0
    def _choose_best_discard_in_tempai(self, tiles, melds, discard_options):
        # first of all we find tiles that have the best hand cost * ukeire value
        call_riichi = not self.player.is_open_hand

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

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

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

            is_furiten = self._is_discard_option_furiten(discard_option)

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

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

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

                tanki_type = None

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

                    if is_pair(hand_set):
                        is_tanki = True

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

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

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

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

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

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

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

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

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

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

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

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

        # if we have several options that give us similar wait
        # FIXME: 1. we find the safest tile to discard
        # FIXME: 2. if safeness is the same, we try to discard non-dora tiles
        return best_discard_desc[0]['discard_option']
Пример #27
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]
Пример #28
0
    def _should_call_riichi_one_sided(self):
        count_tiles = self.player.ai.hand_builder.count_tiles(
            self.player.ai.waiting, TilesConverter.to_34_array(self.player.closed_hand)
        )
        waiting = self.player.ai.waiting[0]
        hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False)

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

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

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

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

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

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

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

                return False

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

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

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

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

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

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

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

                    return True

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

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

                    return True

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

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

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

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

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

                    return True

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

        simplified_waiting = simplify(waiting)

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

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

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

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

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

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

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

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

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

            is_furiten = self._is_discard_option_furiten(discard_option)

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

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

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

                tanki_type = None

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

                    if is_pair(hand_set):
                        is_tanki = True

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

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

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

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

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

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

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

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

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

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

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

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

        # if we have several options that give us similar wait
        # FIXME: 1. we find the safest tile to discard
        # FIXME: 2. if safeness is the same, we try to discard non-dora tiles
        return best_discard_desc[0]['discard_option']
Пример #30
0
    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)
Пример #31
0
    def _find_best_meld_to_open(self, call_tile_136, possible_melds, new_tiles, closed_hand, discarded_tile):
        all_tiles_are_suitable = True
        for tile_136 in closed_hand:
            all_tiles_are_suitable &= self.is_tile_suitable(tile_136)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.player.logger.debug(
            log.MELD_PREPARE,
            "Tiles could be used for open meld",
            context=final_results,
        )
        return final_results[0]
Пример #32
0
    def calculate_fu(
        self,
        hand,
        win_tile,
        win_group,
        config,
        valued_tiles=None,
        melds=None,
    ):
        """
        Calculate hand fu with explanations
        :param hand:
        :param win_tile: 136 tile format
        :param win_group: one set where win tile exists
        :param config: HandConfig object
        :param valued_tiles: dragons, player wind, round wind
        :param melds: opened sets
        :return:
        """

        win_tile_34 = win_tile // 4

        if not valued_tiles:
            valued_tiles = []

        if not melds:
            melds = []

        fu_details = []

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

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

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

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

        if win_group in closed_chi_sets:
            tile_index = simplify(win_tile_34)

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

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

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

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

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

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

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

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

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

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

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

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

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

        return fu_details, self.round_fu(fu_details)
Пример #33
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]
            ]),
        }
Пример #34
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)
Пример #35
0
    def _choose_best_discard_in_tempai(self, discard_options, after_meld):
        discard_desc = []

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

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

            is_furiten = self._is_discard_option_furiten(discard_option)

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

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

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

                tanki_type = None

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

                    if is_pair(hand_set):
                        is_tanki = True

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

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

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

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

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

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

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

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

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

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

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

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

        # if we have several options that give us similar wait
        return best_discard_desc[0]["discard_option"]