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

        dora_count_man_not_isolated = 0
        dora_count_pin_not_isolated = 0
        dora_count_sou_not_isolated = 0

        for tile_136 in tiles_136:
            tile_34 = tile_136 // 4

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

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

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

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

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

        if suit['name'] == 'pin':
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated
        elif suit['name'] == 'sou':
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated
        elif suit['name'] == 'man':
            self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated
    def get_possible_meld(self, tile, is_kamicha_discard):

        closed_hand = self.player.closed_hand[:]

        # we can't open hand anymore
        if len(closed_hand) == 1:
            return None, None

        discarded_tile = tile // 4
        closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile])

        combinations = []
        first_index = 0
        second_index = 0
        if is_man(discarded_tile):
            first_index = 0
            second_index = 8
        elif is_pin(discarded_tile):
            first_index = 9
            second_index = 17
        elif is_sou(discarded_tile):
            first_index = 18
            second_index = 26

        if second_index == 0:
            # honor tiles
            if closed_hand_34[discarded_tile] == 3:
                combinations = [[[discarded_tile] * 3]]
        else:
            # to avoid not necessary calculations
            # we can check only tiles around +-2 discarded tile
            first_limit = discarded_tile - 2
            if first_limit < first_index:
                first_limit = first_index

            second_limit = discarded_tile + 2
            if second_limit > second_index:
                second_limit = second_index

            combinations = self.hand_divider.find_valid_combinations(
                closed_hand_34, first_limit, second_limit, True)

        if combinations:
            combinations = combinations[0]
        # possible_melds = []
        melds_chi, melds_pon = [], []
        for best_meld_34 in combinations:
            # we can call pon from everyone
            if is_pon(best_meld_34) and discarded_tile in best_meld_34:
                if best_meld_34 not in melds_pon:
                    melds_pon.append(best_meld_34)

            # we can call chi only from left player
            if is_chi(
                    best_meld_34
            ) and is_kamicha_discard and discarded_tile in best_meld_34:
                if best_meld_34 not in melds_chi:
                    melds_chi.append(best_meld_34)

        return melds_chi, melds_pon
Example #3
0
    def is_condition_met(self, hand, *args):
        pon_sets = [i for i in hand if is_pon_or_kan(i)]
        if len(pon_sets) < 3:
            return False

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

        for sou_item in sou_pon:
            for pin_item in pin_pon:
                for man_item in man_pon:
                    # cast tile indices to 1..9 representation
                    sou_item = set([simplify(x) for x in sou_item])
                    pin_item = set([simplify(x) for x in pin_item])
                    man_item = set([simplify(x) for x in man_item])
                    if sou_item == pin_item == man_item:
                        return True
        return False
Example #4
0
    def is_condition_met(self, hand, allow_other_sets, *args):
        sou_sets = 0
        pin_sets = 0
        man_sets = 0
        honor_sets = 0
        for item in hand:
            if is_sou(item[0]):
                sou_sets += 1
            elif is_pin(item[0]):
                pin_sets += 1
            elif is_man(item[0]):
                man_sets += 1
            else:
                honor_sets += 1

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

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

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

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

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

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

        for sou_item in sou_pon:
            for pin_item in pin_pon:
                for man_item in man_pon:
                    # cast tile indices to 1..9 representation
                    sou_item = [simplify(x) for x in sou_item]
                    pin_item = [simplify(x) for x in pin_item]
                    man_item = [simplify(x) for x in man_item]
                    if sou_item == pin_item == man_item:
                        return True
        return False
Example #6
0
    def is_chinitsu(self, hand):
        """
        The hand contains tiles from a single suit
        :param hand: list of hand's sets
        :return: true|false
        """
        honor_sets = 0
        sou_sets = 0
        pin_sets = 0
        man_sets = 0
        for item in hand:
            if item[0] in HONOR_INDICES:
                honor_sets += 1

            if is_sou(item[0]):
                sou_sets += 1
            elif is_pin(item[0]):
                pin_sets += 1
            elif is_man(item[0]):
                man_sets += 1

        sets = [sou_sets, pin_sets, man_sets]
        only_one_suit = len([x for x in sets if x != 0]) == 1

        return only_one_suit and honor_sets == 0
Example #7
0
    def _suits_tiles(self, tiles_34):
        """
        Return tiles separated by suits
        :param tiles_34:
        :return:
        """
        suits = [
            [0] * 9,
            [0] * 9,
            [0] * 9,
        ]

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

            suit_index = None
            simplified_tile = simplify(tile)

            if is_man(tile):
                suit_index = 0

            if is_pin(tile):
                suit_index = 1

            if is_sou(tile):
                suit_index = 2

            suits[suit_index][simplified_tile] += total_tiles

        return suits
Example #8
0
    def is_sanshoku(self, hand):
        """
        The same chi in three suits
        :param hand: list of hand's sets
        :return: true|false
        """
        chi_sets = [i for i in hand if is_chi(i)]
        if len(chi_sets) < 3:
            return False

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

        for sou_item in sou_chi:
            for pin_item in pin_chi:
                for man_item in man_chi:
                    # cast tile indices to 0..8 representation
                    sou_item = [simplify(x) for x in sou_item]
                    pin_item = [simplify(x) for x in pin_item]
                    man_item = [simplify(x) for x in man_item]
                    if sou_item == pin_item == man_item:
                        return True
        return False
Example #9
0
    def _initialize_honitsu_dora_count(self, tiles_136, suit):
        tiles_34 = TilesConverter.to_34_array(tiles_136)

        dora_count_man_not_isolated = 0
        dora_count_pin_not_isolated = 0
        dora_count_sou_not_isolated = 0

        for tile_136 in tiles_136:
            tile_34 = tile_136 // 4

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

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

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

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

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

        if suit['name'] == 'pin':
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated
        elif suit['name'] == 'sou':
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated
        elif suit['name'] == 'man':
            self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated
Example #10
0
    def is_condition_met(self, hand, *args):
        chi_sets = [i for i in hand if is_chi(i)]
        if len(chi_sets) < 3:
            return False

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

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

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

        sets = [sou_chi, pin_chi, man_chi]

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

            casted_sets = []

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

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

        return False
Example #12
0
    def is_chuuren_poutou(self, hand):
        """
        The hand contains 1-1-1-2-3-4-5-6-7-8-9-9-9 of one suit, plus any other tile of the same suit.
        :param hand: list of hand's sets
        :return: true|false
        """

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

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

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

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

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

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

        if len(indices) == 1:
            return True

        return False
Example #13
0
    def find_suji(self, safe_tiles_34):
        suji = []
        suits = [[], [], []]

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

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

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

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

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

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

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

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

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

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

        suji = list(set(suji))

        return suji
    def find_suji(self, safe_tiles_34):
        suji = []
        suits = [[], [], []]

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

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

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

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

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

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

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

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

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

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

        suji = list(set(suji))

        return suji
Example #15
0
    def is_condition_met(self, hand, *args):
        sou_sets = 0
        pin_sets = 0
        man_sets = 0
        honor_sets = 0
        for item in hand:
            if is_sou(item[0]):
                sou_sets += 1
            elif is_pin(item[0]):
                pin_sets += 1
            elif is_man(item[0]):
                man_sets += 1
            else:
                honor_sets += 1

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

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

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

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

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

        if len(indices) == 1:
            return True

        return False
Example #16
0
    def is_ittsu(self, hand):
        """
        Three sets of same suit: 1-2-3, 4-5-6, 7-8-9
        :param hand: list of hand's sets
        :return: true|false
        """

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

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

        sets = [sou_chi, pin_chi, man_chi]

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

            casted_sets = []

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

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

        return False
Example #17
0
    def _initialize_honitsu_dora_count(self, tiles_136, suit):
        tiles_34 = TilesConverter.to_34_array(tiles_136)

        dora_count_man = 0
        dora_count_pin = 0
        dora_count_sou = 0

        dora_count_man_not_isolated = 0
        dora_count_pin_not_isolated = 0
        dora_count_sou_not_isolated = 0

        for tile_136 in tiles_136:
            tile_34 = tile_136 // 4

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

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

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

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

        if suit["name"] == "pin":
            self.dora_count_our_suit = dora_count_pin
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated
        elif suit["name"] == "sou":
            self.dora_count_our_suit = dora_count_sou
            self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated
        elif suit["name"] == "man":
            self.dora_count_our_suit = dora_count_man
            self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated
Example #18
0
    def is_condition_met(self, hand, *args):
        honor_sets = 0
        sou_sets = 0
        pin_sets = 0
        man_sets = 0
        for item in hand:
            if item[0] in HONOR_INDICES:
                honor_sets += 1

            if is_sou(item[0]):
                sou_sets += 1
            elif is_pin(item[0]):
                pin_sets += 1
            elif is_man(item[0]):
                man_sets += 1

        sets = [sou_sets, pin_sets, man_sets]
        only_one_suit = len([x for x in sets if x != 0]) == 1

        return only_one_suit and honor_sets == 0
Example #19
0
    def find_tiles_to_discard(self, players):
        found_suji = []
        for player in players:
            suji = []
            suits = [[], [], []]

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

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

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

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

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

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

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

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

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

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

            suji = list(set(suji))

            found_suji.append(suji)

        if not found_suji:
            return []

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

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

            tiles.extend(self._suji_tiles(suji))

        return tiles
Example #20
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]
Example #21
0
    def try_to_call_meld(self, tile, is_kamicha_discard):
        """
        When bot can open hand with a set (chi or pon/kan) this method will be called
        :param tile: 136 format tile
        :param is_kamicha_discard: boolean
        :return: Meld and DiscardOption objects or None, None
        """

        # can't call if in riichi
        if self.player.in_riichi:
            return None, None

        closed_hand = self.player.closed_hand[:]

        # check for appropriate hand size, seems to solve a bug
        if len(closed_hand) == 1:
            return None, None

        # get old shanten value
        old_tiles_34 = TilesConverter.to_34_array(self.player.tiles)
        old_shanten = self.shanten.calculate_shanten(old_tiles_34, self.player.open_hand_34_tiles)

        # setup
        discarded_tile = tile // 4
        new_closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile])

        # We will use hand_divider to find possible melds involving the discarded tile.
        # Check its suit and number to narrow the search conditions
        # skipping this will break the default mahjong functions
        combinations = []
        first_index = 0
        second_index = 0
        if is_man(discarded_tile):
            first_index = 0
            second_index = 8
        elif is_pin(discarded_tile):
            first_index = 9
            second_index = 17
        elif is_sou(discarded_tile):
            first_index = 18
            second_index = 26

        if second_index == 0:
            # honor tiles
            if new_closed_hand_34[discarded_tile] == 3:
                combinations = [[[discarded_tile] * 3]]
        else:
            # to avoid not necessary calculations
            # we can check only tiles around +-2 discarded tile
            first_limit = discarded_tile - 2
            if first_limit < first_index:
                first_limit = first_index

            second_limit = discarded_tile + 2
            if second_limit > second_index:
                second_limit = second_index

            combinations = self.hand_divider.find_valid_combinations(new_closed_hand_34,
                                                                           first_limit,
                                                                           second_limit, True)
        # Reduce combinations to list of melds
        if combinations:
            combinations = combinations[0]

        # Verify that a meld can be called
        possible_melds = []
        for meld_34 in combinations:
            # we can call pon from everyone
            if is_pon(meld_34) and discarded_tile in meld_34:
                if meld_34 not in possible_melds:
                    possible_melds.append(meld_34)

            # we can call chi only from left player
            if is_chi(meld_34) and is_kamicha_discard and discarded_tile in meld_34:
                if meld_34 not in possible_melds:
                    possible_melds.append(meld_34)

        # For each possible meld, check if calling it and discarding can improve shanten
        new_shanten = float('inf')
        discard_136 = None
        tiles = None

        for meld_34 in possible_melds:
            shanten, disc = self.meldDiscard(meld_34, tile)
            if shanten < new_shanten:
                new_shanten, discard_136 = shanten, disc
                tiles = meld_34

        # If shanten can be improved by calling meld, call it
        if new_shanten < old_shanten:
            meld = Meld()
            meld.type = is_chi(tiles) and Meld.CHI or Meld.PON

            # convert meld tiles back to 136 format for Meld type return
            # find them in a copy of the closed hand and remove
            tiles.remove(discarded_tile)

            first_tile = TilesConverter.find_34_tile_in_136_array(tiles[0], closed_hand)
            closed_hand.remove(first_tile)

            second_tile = TilesConverter.find_34_tile_in_136_array(tiles[1], closed_hand)
            closed_hand.remove(second_tile)

            tiles_136 = [
                first_tile,
                second_tile,
                tile
            ]

            discard_136 = TilesConverter.find_34_tile_in_136_array(discard_136 // 4, closed_hand)
            meld.tiles = sorted(tiles_136)
            return meld, discard_136

        return None, None
Example #22
0
    def try_to_call_meld(self, tile, is_kamicha_discard):
        """
        Determine should we call a meld or not.
        If yes, it will return Meld object and tile to discard
        :param tile: 136 format tile
        :param is_kamicha_discard: boolean
        :return: Meld and DiscardOption objects
        """
        if self.player.in_riichi:
            return None, None

        if self.player.ai.in_defence:
            return None, None

        closed_hand = self.player.closed_hand[:]

        # we can't open hand anymore
        if len(closed_hand) == 1:
            return None, None

        # we can't use this tile for our chosen strategy
        if not self.is_tile_suitable(tile):
            return None, None

        discarded_tile = tile // 4
        new_tiles = self.player.tiles[:] + [tile]
        closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile])

        combinations = []
        first_index = 0
        second_index = 0
        if is_man(discarded_tile):
            first_index = 0
            second_index = 8
        elif is_pin(discarded_tile):
            first_index = 9
            second_index = 17
        elif is_sou(discarded_tile):
            first_index = 18
            second_index = 26

        if second_index == 0:
            # honor tiles
            if closed_hand_34[discarded_tile] == 3:
                combinations = [[[discarded_tile] * 3]]
        else:
            # to avoid not necessary calculations
            # we can check only tiles around +-2 discarded tile
            first_limit = discarded_tile - 2
            if first_limit < first_index:
                first_limit = first_index

            second_limit = discarded_tile + 2
            if second_limit > second_index:
                second_limit = second_index

            combinations = self.player.ai.hand_divider.find_valid_combinations(closed_hand_34,
                                                                               first_limit,
                                                                               second_limit, True)

        if combinations:
            combinations = combinations[0]

        possible_melds = []
        for best_meld_34 in combinations:
            # we can call pon from everyone
            if is_pon(best_meld_34) and discarded_tile in best_meld_34:
                if best_meld_34 not in possible_melds:
                    possible_melds.append(best_meld_34)

            # we can call chi only from left player
            if is_chi(best_meld_34) and is_kamicha_discard and discarded_tile in best_meld_34:
                if best_meld_34 not in possible_melds:
                    possible_melds.append(best_meld_34)

        # we can call melds only with allowed tiles
        validated_melds = []
        for meld in possible_melds:
            if (self.is_tile_suitable(meld[0] * 4) and
                    self.is_tile_suitable(meld[1] * 4) and
                    self.is_tile_suitable(meld[2] * 4)):
                validated_melds.append(meld)
        possible_melds = validated_melds

        if not possible_melds:
            return None, None

        best_meld_34 = self._find_best_meld_to_open(possible_melds, new_tiles)
        if best_meld_34:
            # we need to calculate count of shanten with supposed meld
            # to prevent bad hand openings
            melds = self.player.open_hand_34_tiles + [best_meld_34]
            outs_results, shanten = self.player.ai.calculate_outs(new_tiles, closed_hand, melds)

            # each strategy can use their own value to min shanten number
            if shanten > self.min_shanten:
                return None, None

            # we can't improve hand, so we don't need to open it
            if not outs_results:
                return None, None

            # sometimes we had to call tile, even if it will not improve our hand
            # otherwise we can call only with improvements of shanten
            if not self.meld_had_to_be_called(tile) and shanten >= self.player.ai.previous_shanten:
                return None, None

            meld_type = is_chi(best_meld_34) and Meld.CHI or Meld.PON
            best_meld_34.remove(discarded_tile)

            first_tile = TilesConverter.find_34_tile_in_136_array(best_meld_34[0], closed_hand)
            closed_hand.remove(first_tile)

            second_tile = TilesConverter.find_34_tile_in_136_array(best_meld_34[1], closed_hand)
            closed_hand.remove(second_tile)

            tiles = [
                first_tile,
                second_tile,
                tile
            ]

            meld = Meld()
            meld.type = meld_type
            meld.tiles = sorted(tiles)

            # we had to be sure that all our discard results exists in the closed hand
            filtered_results = []
            for result in outs_results:
                if result.find_tile_in_hand(closed_hand):
                    filtered_results.append(result)

            # we can't discard anything, so let's not open our hand
            if not filtered_results:
                return None, None

            selected_tile = self.player.ai.process_discard_options_and_select_tile_to_discard(
                filtered_results,
                shanten,
                had_was_open=True
            )

            return meld, selected_tile

        return None, None
    def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles):
        """
        Determine should we call a meld or not.
        If yes, it will return Meld object and tile to discard
        :param tile: 136 format tile
        :param is_kamicha_discard: boolean
        :param new_tiles:
        :return: Meld and DiscardOption objects
        """
        if self.player.in_riichi:
            return None, None

        if self.player.ai.in_defence:
            return None, None

        closed_hand = self.player.closed_hand[:]

        # we can't open hand anymore
        if len(closed_hand) == 1:
            return None, None

        # we can't use this tile for our chosen strategy
        if not self.is_tile_suitable(tile):
            return None, None

        discarded_tile = tile // 4
        closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile])

        combinations = []
        first_index = 0
        second_index = 0
        if is_man(discarded_tile):
            first_index = 0
            second_index = 8
        elif is_pin(discarded_tile):
            first_index = 9
            second_index = 17
        elif is_sou(discarded_tile):
            first_index = 18
            second_index = 26

        if second_index == 0:
            # honor tiles
            if closed_hand_34[discarded_tile] == 3:
                combinations = [[[discarded_tile] * 3]]
        else:
            # to avoid not necessary calculations
            # we can check only tiles around +-2 discarded tile
            first_limit = discarded_tile - 2
            if first_limit < first_index:
                first_limit = first_index

            second_limit = discarded_tile + 2
            if second_limit > second_index:
                second_limit = second_index

            combinations = self.player.ai.hand_divider.find_valid_combinations(
                closed_hand_34,
                first_limit,
                second_limit,
                True
            )

        if combinations:
            combinations = combinations[0]

        possible_melds = []
        for best_meld_34 in combinations:
            # we can call pon from everyone
            if is_pon(best_meld_34) and discarded_tile in best_meld_34:
                if best_meld_34 not in possible_melds:
                    possible_melds.append(best_meld_34)

            # we can call chi only from left player
            if is_chi(best_meld_34) and is_kamicha_discard and discarded_tile in best_meld_34:
                if best_meld_34 not in possible_melds:
                    possible_melds.append(best_meld_34)

        # we can call melds only with allowed tiles
        validated_melds = []
        for meld in possible_melds:
            if (self.is_tile_suitable(meld[0] * 4) and
                    self.is_tile_suitable(meld[1] * 4) and
                    self.is_tile_suitable(meld[2] * 4)):
                validated_melds.append(meld)
        possible_melds = validated_melds

        if not possible_melds:
            return None, None

        chosen_meld = self._find_best_meld_to_open(possible_melds, new_tiles, closed_hand, tile)
        selected_tile = chosen_meld['discard_tile']
        meld = chosen_meld['meld']

        shanten = selected_tile.shanten
        had_to_be_called = self.meld_had_to_be_called(tile)
        had_to_be_called = had_to_be_called or selected_tile.had_to_be_discarded

        # each strategy can use their own value to min shanten number
        if shanten > self.min_shanten:
            return None, None

        # sometimes we had to call tile, even if it will not improve our hand
        # otherwise we can call only with improvements of shanten
        if not had_to_be_called and shanten >= self.player.ai.shanten:
            return None, None

        return meld, selected_tile
Example #24
0
    def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles):
        """
        Determine should we call a meld or not.
        If yes, it will return MeldPrint object and tile to discard
        :param tile: 136 format tile
        :param is_kamicha_discard: boolean
        :param new_tiles:
        :return: MeldPrint and DiscardOption objects
        """
        if self.player.in_riichi:
            return None, None

        closed_hand = self.player.closed_hand[:]

        # we can't open hand anymore
        if len(closed_hand) == 1:
            return None, None

        # we can't use this tile for our chosen strategy
        if not self.is_tile_suitable(tile):
            return None, None

        discarded_tile = tile // 4
        closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile])

        combinations = []
        first_index = 0
        second_index = 0
        if is_man(discarded_tile):
            first_index = 0
            second_index = 8
        elif is_pin(discarded_tile):
            first_index = 9
            second_index = 17
        elif is_sou(discarded_tile):
            first_index = 18
            second_index = 26

        if second_index == 0:
            # honor tiles
            if closed_hand_34[discarded_tile] == 3:
                combinations = [[[discarded_tile] * 3]]
        else:
            # to avoid not necessary calculations
            # we can check only tiles around +-2 discarded tile
            first_limit = discarded_tile - 2
            if first_limit < first_index:
                first_limit = first_index

            second_limit = discarded_tile + 2
            if second_limit > second_index:
                second_limit = second_index

            combinations = self.player.ai.hand_divider.find_valid_combinations(
                closed_hand_34, first_limit, second_limit, True
            )

        if combinations:
            combinations = combinations[0]

        possible_melds = []
        for best_meld_34 in combinations:
            # we can call pon from everyone
            if is_pon(best_meld_34) and discarded_tile in best_meld_34:
                if best_meld_34 not in possible_melds:
                    possible_melds.append(best_meld_34)

            # we can call chi only from left player
            if is_chi(best_meld_34) and is_kamicha_discard and discarded_tile in best_meld_34:
                if best_meld_34 not in possible_melds:
                    possible_melds.append(best_meld_34)

        # we can call melds only with allowed tiles
        validated_melds = []
        for meld in possible_melds:
            if (
                self.is_tile_suitable(meld[0] * 4)
                and self.is_tile_suitable(meld[1] * 4)
                and self.is_tile_suitable(meld[2] * 4)
            ):
                validated_melds.append(meld)
        possible_melds = validated_melds

        if not possible_melds:
            return None, None

        chosen_meld_dict = self._find_best_meld_to_open(tile, possible_melds, new_tiles, closed_hand, tile)
        # we didn't find a good discard candidate after open meld
        if not chosen_meld_dict:
            return None, None

        selected_tile = chosen_meld_dict["discard_tile"]
        meld = chosen_meld_dict["meld"]

        shanten = selected_tile.shanten
        had_to_be_called = self.meld_had_to_be_called(tile)
        had_to_be_called = had_to_be_called or selected_tile.had_to_be_discarded

        # each strategy can use their own value to min shanten number
        if shanten > self.min_shanten:
            self.player.logger.debug(
                log.MELD_DEBUG,
                "After meld shanten is too high for our strategy. Abort melding.",
            )
            return None, None

        # sometimes we had to call tile, even if it will not improve our hand
        # otherwise we can call only with improvements of shanten
        if not had_to_be_called and shanten >= self.player.ai.shanten:
            self.player.logger.debug(
                log.MELD_DEBUG,
                "Meld is not improving hand shanten. Abort melding.",
            )
            return None, None

        if not self.validate_meld(chosen_meld_dict):
            self.player.logger.debug(
                log.MELD_DEBUG,
                "Meld is suitable for strategy logic. Abort melding.",
            )
            return None, None

        if not self.should_push_against_threats(chosen_meld_dict):
            self.player.logger.debug(
                log.MELD_DEBUG,
                "Meld is too dangerous to call. Abort melding.",
            )
            return None, None

        return meld, selected_tile
    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)
Example #26
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)
Example #27
0
    def find_suji(self, tiles_136):
        tiles_34 = list(set([x // 4 for x in tiles_136]))

        suji = []
        suits = [[], [], []]

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

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

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

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

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

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

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

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

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

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

        all_suji = list(set(suji))
        result = []
        for suji in all_suji:
            suji_temp = suji % 9
            base = suji - suji_temp - 1

            if suji_temp == self.FIRST_SUJI:
                result += [base + 1, base + 4, base + 7]

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

            if suji_temp == self.THIRD_SUJI:
                result += [base + 3, base + 6, base + 9]

        return result