def _check_discard_order(self, suit, early_position):
        # let's check the following considiton:
        # if enemy had discarded tiles from that suit or honor and after that he had discarded a tile from a different
        # suit from his hand - let's believe it's not honitsu
        suit_discards_positions = [
            self.enemy.discards.index(x) for x in self.enemy.discards
            if suit["function"](x.value // 4)
        ]
        if suit_discards_positions:
            # we consider second discard of chosen suit to be reference point
            # first one could have happened when player was not yet sure if he is going to honitsu
            # after the second one there should be no discars of other suit from hand
            reference_discard = suit_discards_positions[min(
                1,
                len(suit_discards_positions) - 1)]
            discards_after = self.enemy.discards[reference_discard:]
            if discards_after:
                has_discarded_other_suit_from_hand = [
                    x for x in discards_after
                    if (not x.is_tsumogiri and not is_honor(x.value // 4)
                        and not suit["function"](x.value // 4))
                ]
                if has_discarded_other_suit_from_hand:
                    return False

            # if we started discards suit tiles early, it's probably not honitsu
            if suit_discards_positions[0] <= early_position:
                return False

        # discard order seems similar to honitsu/chinitsu one
        return True
    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
Beispiel #3
0
    def calculate_dora_count(self, tiles_136):
        self.dora_count_central = 0
        self.dora_count_not_central = 0
        self.aka_dora_count = 0

        for tile_136 in tiles_136:
            tile_34 = tile_136 // 4

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

            if not dora_count:
                continue

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

        self.dora_count_central += self.aka_dora_count
        self.dora_count_total = self.dora_count_central + self.dora_count_not_central
 def is_tile_suitable(self, tile):
     """
     We can use only tiles of chosen suit and honor tiles
     :param tile: 136 tiles format
     :return: True
     """
     tile //= 4
     return self.chosen_suit(tile) or is_honor(tile)
Beispiel #5
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
Beispiel #6
0
 def is_tile_suitable(self, tile):
     """
     We can use only tiles of chosen suit and honor tiles
     :param tile: 136 tiles format
     :return: True
     """
     tile //= 4
     return self.chosen_suit(tile) or is_honor(tile)
Beispiel #7
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
Beispiel #8
0
    def _mark_safe_tiles_against_honitsu(self, player):
        against_honitsu = []
        for tile in range(0, 34):
            if not self.closed_hand_34[tile]:
                continue

            if not player.chosen_suit(tile) and not is_honor(tile):
                against_honitsu.append(tile)
        return against_honitsu
Beispiel #9
0
    def get_safe_tiles_34(self):
        safe_tiles = []
        for x in range(0, 34):
            if not is_honor(x):
                safe_tiles.append(x)
            elif not self.enemy.valued_honors.count(x):
                safe_tiles.append(x)

        return safe_tiles
    def _mark_safe_tiles_against_honitsu(self, player):
        against_honitsu = []
        for tile in range(0, 34):
            if not self.closed_hand_34[tile]:
                continue

            if not player.chosen_suit(tile) and not is_honor(tile):
                against_honitsu.append(tile)
        return against_honitsu
    def get_safe_tiles_34(self):
        if not self.chosen_suit:
            return []

        safe_tiles = []
        for x in range(0, 34):
            if not self.chosen_suit(x) and not is_honor(x):
                safe_tiles.append(x)

        return safe_tiles
Beispiel #12
0
    def _calculate_not_suitable_tiles_cnt(self, tiles_34, suit):
        self.tiles_count_other_suits = 0
        self.tiles_count_other_suits_not_isolated = 0

        for x in range(0, 34):
            tile = tiles_34[x]
            if not tile:
                continue

            if not suit(x) and not is_honor(x):
                self.tiles_count_other_suits += tile
                if not is_tile_strictly_isolated(tiles_34, x):
                    self.tiles_count_other_suits_not_isolated += tile
    def _calculate_not_suitable_tiles_cnt(self, tiles_34, suit):
        self.tiles_count_other_suits = 0
        self.tiles_count_other_suits_not_isolated = 0

        for x in range(0, 34):
            tile = tiles_34[x]
            if not tile:
                continue

            if not suit(x) and not is_honor(x):
                self.tiles_count_other_suits += tile
                if not is_tile_strictly_isolated(tiles_34, x):
                    self.tiles_count_other_suits_not_isolated += tile
    def meld_had_to_be_called(self, tile):
        has_not_suitable_tiles = False

        for hand_tile in self.player.tiles:
            if not self.is_tile_suitable(hand_tile):
                has_not_suitable_tiles = True
                break

        # if we still have unsuitable tiles, let's call honor pons
        # even if they don't change number of shanten
        if has_not_suitable_tiles and is_honor(tile // 4):
            return True

        return False
    def meld_had_to_be_called(self, tile):
        has_not_suitable_tiles = False

        for hand_tile in self.player.tiles:
            if not self.is_tile_suitable(hand_tile):
                has_not_suitable_tiles = True
                break

        # if we still have unsuitable tiles, let's call honor pons
        # even if they don't change number of shanten
        if has_not_suitable_tiles and is_honor(tile // 4):
            return True

        return False
    def get_bonus_danger(self, tile_136, number_of_revealed_tiles):
        tile_34 = tile_136 // 4

        if is_honor(tile_34):
            if number_of_revealed_tiles == 4:
                return []
            elif number_of_revealed_tiles == 3:
                return [TileDanger.HONITSU_THIRD_HONOR_BONUS_DANGER]
            elif number_of_revealed_tiles == 2:
                return [TileDanger.HONITSU_SECOND_HONOR_BONUS_DANGER]
            else:
                return [TileDanger.HONITSU_SHONPAI_HONOR_BONUS_DANGER]

        return []
Beispiel #17
0
def test_is_threatening_and_atodzuke():
    table = Table()

    threatening_players = table.player.ai.defence.get_threatening_players()
    assert len(threatening_players) == 0

    table.add_dora_indicator(string_to_136_tile(honors="5"))

    enemy_seat = 2
    table.add_called_meld(enemy_seat, make_meld(MeldPrint.CHI, man="234"))
    table.add_called_meld(enemy_seat, make_meld(MeldPrint.PON, sou="333"))
    table.add_called_meld(enemy_seat, make_meld(MeldPrint.KAN, pin="9999"))
    table.player.round_step = 5

    table.add_discarded_tile(enemy_seat, string_to_136_tile(honors="1"), False)
    table.add_discarded_tile(enemy_seat, string_to_136_tile(honors="4"), False)
    table.add_discarded_tile(enemy_seat, string_to_136_tile(sou="8"), False)
    table.add_discarded_tile(enemy_seat, string_to_136_tile(pin="9"), False)
    table.add_discarded_tile(enemy_seat, string_to_136_tile(sou="1"), False)
    table.add_discarded_tile(enemy_seat, string_to_136_tile(man="6"), False)

    # atodzuke with 3 melds is a threat
    threatening_players = table.player.ai.defence.get_threatening_players()
    assert len(threatening_players) == 1
    assert threatening_players[0].enemy.seat == enemy_seat
    assert threatening_players[0].threat_reason[
        "id"] == EnemyDanger.THREAT_OPEN_HAND_UNKNOWN_COST["id"]
    assert threatening_players[0].get_assumed_hand_cost(
        string_to_136_tile(honors="5")) == 2000
    assert threatening_players[0].get_assumed_hand_cost(
        string_to_136_tile(honors="6")) == 8000

    for tile_136 in range(0, 136):
        bonus_danger = threatening_players[0].threat_reason.get(
            "active_yaku")[0].get_bonus_danger(tile_136, 1)
        if not is_honor(tile_136 // 4):
            assert not bonus_danger
        elif ((tile_136 // 4 == string_to_34_tile(honors="1"))
              or (tile_136 // 4 == string_to_34_tile(honors="3"))
              or (tile_136 // 4 == string_to_34_tile(honors="5"))
              or (tile_136 // 4 == string_to_34_tile(honors="6"))
              or (tile_136 // 4 == string_to_34_tile(honors="7"))):
            assert bonus_danger
        else:
            assert not bonus_danger
Beispiel #18
0
def test_is_threatening_and_honitsu_hand():
    table = Table()
    table.add_dora_indicator(string_to_136_tile(pin="1"))

    threatening_players = table.player.ai.defence.get_threatening_players()
    assert len(threatening_players) == 0

    enemy_seat = 1
    table.add_called_meld(enemy_seat, make_meld(MeldPrint.PON, honors="444"))
    table.add_called_meld(enemy_seat, make_meld(MeldPrint.CHI, pin="123"))
    table.add_called_meld(enemy_seat, make_meld(MeldPrint.CHI, pin="345"))

    table.add_discarded_tile(enemy_seat, string_to_136_tile(sou="1"), False)
    table.add_discarded_tile(enemy_seat, string_to_136_tile(sou="5"), False)
    table.add_discarded_tile(enemy_seat, string_to_136_tile(sou="8"), False)
    table.add_discarded_tile(enemy_seat, string_to_136_tile(sou="9"), False)
    table.add_discarded_tile(enemy_seat, string_to_136_tile(man="1"), False)
    table.add_discarded_tile(enemy_seat, string_to_136_tile(man="1"), False)
    table.add_discarded_tile(enemy_seat, string_to_136_tile(pin="1"), False)

    threatening_players = table.player.ai.defence.get_threatening_players()
    assert len(threatening_players) == 1
    assert threatening_players[0].threat_reason[
        "id"] == EnemyDanger.THREAT_EXPENSIVE_OPEN_HAND["id"]
    assert threatening_players[0].get_assumed_hand_cost(
        string_to_136_tile(pin="4")) == 3900
    assert threatening_players[0].get_assumed_hand_cost(
        string_to_136_tile(pin="2")) == 8000
    assert HonitsuAnalyzer.id in [
        x.id for x in threatening_players[0].threat_reason["active_yaku"]
    ]

    honitsu_analyzer = [
        x for x in threatening_players[0].threat_reason["active_yaku"]
        if x.id == HonitsuAnalyzer.id
    ][0]

    for tile_136 in range(0, 136):
        bonus_danger = honitsu_analyzer.get_bonus_danger(tile_136, 1)
        if is_honor(tile_136 // 4):
            assert bonus_danger
        else:
            assert not bonus_danger
    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 _get_dora_scale_bonus(self, tile_136):
        tile_34 = tile_136 // 4
        scale_bonus = 0

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

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

        return scale_bonus
    def _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
    def should_activate_strategy(self, tiles_136, meld_tile=None):
        """
        We can go for chinitsu strategy if we have prevalence of one suit
        """

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

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

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

        count_of_shuntsu_other_suits = 0
        count_of_koutsu_other_suits = 0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.chosen_suit = suit["function"]

        return True
    def should_activate_strategy(self, tiles_136, meld_tile=None):
        """
        Tanyao hand is a hand without terminal and honor tiles, to achieve this
        we will use different approaches
        :return: boolean
        """

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

        tiles = TilesConverter.to_34_array(self.player.tiles)

        closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand)
        isolated_tiles = [
            x // 4 for x in self.player.tiles
            if is_tile_strictly_isolated(closed_hand_34, x //
                                         4) or is_honor(x // 4)
        ]

        count_of_terminal_pon_sets = 0
        count_of_terminal_pairs = 0
        count_of_valued_pairs = 0
        count_of_not_suitable_tiles = 0
        count_of_not_suitable_not_isolated_tiles = 0
        for x in range(0, 34):
            tile = tiles[x]
            if not tile:
                continue

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

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

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

            if x in self.not_suitable_tiles:
                count_of_not_suitable_tiles += tile

            if x in self.not_suitable_tiles and x not in isolated_tiles:
                count_of_not_suitable_not_isolated_tiles += tile

        # we have too much terminals and honors
        if count_of_not_suitable_tiles >= 5:
            return False

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

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

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

        # 3 or more not suitable tiles that
        # are not isolated is too much
        if count_of_not_suitable_not_isolated_tiles >= 3:
            return False

        # if we are 1 shanten, even 2 tiles
        # that are not suitable and not isolated
        # is too much
        if count_of_not_suitable_not_isolated_tiles >= 2 and self.player.ai.shanten == 1:
            return False

        # TODO: don't open from good 1-shanten into tanyao 1-shaten with same ukeire or worse

        # 123 and 789 indices
        indices = [[0, 1, 2], [6, 7, 8], [9, 10, 11], [15, 16, 17],
                   [18, 19, 20], [24, 25, 26]]

        for index_set in indices:
            first = tiles[index_set[0]]
            second = tiles[index_set[1]]
            third = tiles[index_set[2]]
            if first >= 1 and second >= 1 and third >= 1:
                return False

        # if we have 2 or more non-central doras
        # we don't want to go for tanyao
        if self.dora_count_not_central >= 2:
            return False

        # if we have less than two central doras
        # let's not consider open tanyao
        if self.dora_count_central < 2:
            return False

        # if we have only two central doras let's
        # wait for 5th turn before opening our hand
        if self.dora_count_central == 2 and self.player.round_step < 5:
            return False

        return True
Beispiel #24
0
def boxes_is_chi(boxes):
    items = sorted([t[0] for t in boxes[0:3]])
    if is_honor(items[0]) or is_honor(items[2]):
        return False
    return is_chi(items)
Beispiel #25
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
Beispiel #26
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
    def should_activate_strategy(self, tiles_136):
        """
        We can go for honitsu strategy if we have prevalence of one suit and honor tiles
        """

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

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

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

        suit = suits[0]

        count_of_shuntsu_other_suits = 0
        count_of_koutsu_other_suits = 0

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.chosen_suit = suit['function']

        return True
    def is_yaku_active(self):
        # TODO: in some distant future we may want to analyze menhon as well
        if not self.enemy.melds:
            return False

        total_melds = len(self.enemy.melds)
        total_discards = len(self.enemy.discards)

        # let's check if there is too little info to analyze
        if total_discards < HonitsuAnalyzer.MIN_DISCARD and total_melds < HonitsuAnalyzer.MAX_MELDS:
            return False

        # first of all - check melds, they must be all from one suit or honors
        current_suit = None
        for meld in self.enemy.melds:
            tile = meld.tiles[0]
            tile_34 = tile // 4

            if is_honor(tile_34):
                continue

            suit = ChinitsuAnalyzer._get_tile_suit(tile)
            if not current_suit:
                current_suit = suit
            elif suit["name"] != current_suit["name"]:
                return False

        # let's check discards
        discards = [x.value for x in self.enemy.discards]
        discards_34 = TilesConverter.to_34_array(discards)
        result = count_tiles_by_suits(discards_34)

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

        less_suit = suits[0]
        less_suit_tiles = less_suit["count"]
        percentage_of_less_suit = (less_suit_tiles / total_discards) * 100
        percentage_of_honor_tiles = (honors["count"] / total_discards) * 100

        # there is not too much one suit + honor tiles in the discard
        # so we can tell that user trying to collect honitsu
        if (percentage_of_less_suit <=
                HonitsuAnalyzer.LESS_SUIT_PERCENTAGE_BORDER
                and percentage_of_honor_tiles <=
                HonitsuAnalyzer.HONORS_PERCENTAGE_BORDER):
            if not current_suit:
                current_suit = less_suit
            elif current_suit != less_suit:
                return False

        # still cannot determine the suit - this is probably not honitsu
        if not current_suit:
            return False

        if not self._check_discard_order(
                current_suit,
                int(total_discards / HonitsuAnalyzer.EARLY_DISCARD_DIVISOR)):
            return False

        # all checks have passed - assume this is honitsu
        self.chosen_suit = current_suit["function"]
        return True
    def is_yaku_active(self):
        # TODO: in some distant future we may want to analyze menchin as well
        if not self.enemy.melds:
            return False

        total_melds = len(self.enemy.melds)
        total_discards = len(self.enemy.discards)

        # let's check if there is too little info to analyze
        if total_discards < ChinitsuAnalyzer.MIN_DISCARD and total_melds < ChinitsuAnalyzer.MAX_MELDS:
            return False

        # first of all - check melds, they must be all from one suit
        current_suit = None
        for meld in self.enemy.melds:
            tile = meld.tiles[0]
            tile_34 = tile // 4

            if is_honor(tile_34):
                return False

            suit = self._get_tile_suit(tile)
            if not current_suit:
                current_suit = suit
            elif suit["name"] != current_suit["name"]:
                return False

        assert current_suit

        if not self._check_discard_order(
                current_suit,
                int(total_discards / ChinitsuAnalyzer.EARLY_DISCARD_DIVISOR)):
            return False

        # finally let's check if discard is not too full of chosen suit

        discards = [x.value for x in self.enemy.discards]
        discards_34 = TilesConverter.to_34_array(discards)
        result = count_tiles_by_suits(discards_34)

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

        less_suits = [x for x in suits if x["count"] == suits[0]["count"]]
        assert len(less_suits) != 0

        current_suit_is_less_suit = False
        for less_suit in less_suits:
            if less_suit["name"] == current_suit["name"]:
                current_suit_is_less_suit = True

        if not current_suit_is_less_suit:
            return False

        less_suit = suits[0]
        less_suit_tiles = less_suit["count"]

        if total_discards >= ChinitsuAnalyzer.MIN_DISCARD_FOR_LESS_SUIT:
            percentage_of_less_suit = (less_suit_tiles / total_discards) * 100
            if percentage_of_less_suit > ChinitsuAnalyzer.LESS_SUIT_PERCENTAGE_BORDER:
                return False
        else:
            if len(self.enemy.melds) < 2:
                return False

            if less_suit_tiles > 1:
                return False

        self.chosen_suit = current_suit["function"]
        return True
    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]
    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']
Beispiel #32
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]
    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"]
    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']
    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
Beispiel #36
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 should_activate_strategy(self, tiles_136):
        """
        We can go for chinitsu strategy if we have prevalence of one suit
        """

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

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

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

        count_of_shuntsu_other_suits = 0
        count_of_koutsu_other_suits = 0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.chosen_suit = suit['function']

        return True
Beispiel #38
0
    def calculate_valuation(self):
        # base is 100 for ability to mark tiles as not needed (like set value to 50)
        value = 100
        honored_value = 20

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.valuation = int(value)
    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)