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

        for tile_136 in tiles_136:
            tile_34 = tile_136 // 4

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

            if not dora_count:
                continue

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

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

        for tile_136 in tiles_136:
            tile_34 = tile_136 // 4

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

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

            if not dora_count:
                continue

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

        self.dora_count_central += self.aka_dora_count
        self.dora_count_total = self.dora_count_central + self.dora_count_not_central
Esempio n. 3
0
 def player_can_call_kyuushu_kyuuhai(self, player):
     if len(player.discards) > 0 or len(player.melds) > 0:
         return False
     tiles_34 = [x // 4 for x in player.tiles]
     terminals_and_honors = [
         x for x in tiles_34 if is_honor(x) or is_terminal(x)
     ]
     return len(list(set(terminals_and_honors))) >= 9
Esempio n. 4
0
    def _calculate_assumed_hand_cost(self, tile_136) -> int:
        tile_34 = tile_136 // 4

        melds_han = self.get_melds_han(tile_34)
        if melds_han == 0:
            return 0

        scale_index = melds_han
        scale_index += self.threat_reason.get("dora_count", 0)
        scale_index += self._get_dora_scale_bonus(tile_136)

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

        # add more danger for kan sets (basically it is additional hand cost because of fu)
        for meld in self.enemy.melds:
            if meld.type != Meld.KAN and meld.type != Meld.SHOUMINKAN:
                continue

            if meld.opened:
                # enemy will get additional fu for opened honors or terminals kan
                if is_honor(meld.tiles[0] // 4) or is_terminal(
                        meld.tiles[0] // 4):
                    scale_index += 1
            else:
                # enemy will get additional fu for closed kan
                scale_index += 1

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

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

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

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

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

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

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

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

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

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

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

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

                return False

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

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

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

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

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

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

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

                    return True

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

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

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

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

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

                    return True

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                    return True

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

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

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

        simplified_waiting = simplify(waiting)

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

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

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

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

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

        return True
Esempio n. 7
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
Esempio n. 8
0
    def calculate_tiles_danger(
        self, discard_candidates: List[DiscardOption], enemy_analyzer: EnemyAnalyzer
    ) -> List[DiscardOption]:
        closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)

        safe_against_threat_34 = []

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return discard_candidates