def _make_meld(self, meld_type, tiles):
     meld = Meld()
     meld.who = 0
     meld.type = meld_type
     meld.tiles = tiles
     meld.called_tile = tiles[0]
     return meld
Beispiel #2
0
 def call_meld(self, type, who, from_who, opened, tiles, called_tile):
     meld = Meld()
     meld.type = type
     meld.who = who
     meld.from_who = from_who
     meld.opened = opened
     meld.tiles = tiles
     meld.called_tile = called_tile
     return meld
Beispiel #3
0
    def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand,
                                discarded_tile):
        discarded_tile_34 = discarded_tile // 4

        final_results = []
        for meld_34 in possible_melds:
            meld_34_copy = meld_34.copy()
            closed_hand_copy = closed_hand.copy()

            meld_type = is_chi(meld_34_copy) and Meld.CHI or Meld.PON
            meld_34_copy.remove(discarded_tile_34)

            first_tile = TilesConverter.find_34_tile_in_136_array(
                meld_34_copy[0], closed_hand_copy)
            closed_hand_copy.remove(first_tile)

            second_tile = TilesConverter.find_34_tile_in_136_array(
                meld_34_copy[1], closed_hand_copy)
            closed_hand_copy.remove(second_tile)

            tiles = [first_tile, second_tile, discarded_tile]

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

            melds = self.player.melds + [meld]

            selected_tile = self.player.ai.hand_builder.choose_tile_to_discard(
                new_tiles, closed_hand_copy, melds, print_log=False)

            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
            })

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

        DecisionsLogger.debug(log.MELD_PREPARE,
                              'Options with meld calling',
                              context=final_results)

        return final_results[0]
    def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand, discarded_tile):
        discarded_tile_34 = discarded_tile // 4

        final_results = []
        for meld_34 in possible_melds:
            meld_34_copy = meld_34.copy()
            closed_hand_copy = closed_hand.copy()

            meld_type = is_chi(meld_34_copy) and Meld.CHI or Meld.PON
            meld_34_copy.remove(discarded_tile_34)

            first_tile = TilesConverter.find_34_tile_in_136_array(meld_34_copy[0], closed_hand_copy)
            closed_hand_copy.remove(first_tile)

            second_tile = TilesConverter.find_34_tile_in_136_array(meld_34_copy[1], closed_hand_copy)
            closed_hand_copy.remove(second_tile)

            tiles = [
                first_tile,
                second_tile,
                discarded_tile
            ]

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

            melds = self.player.melds + [meld]

            selected_tile = self.player.ai.hand_builder.choose_tile_to_discard(
                new_tiles,
                closed_hand_copy,
                melds,
                print_log=False
            )

            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
            })

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

        DecisionsLogger.debug(log.MELD_PREPARE, 'Options with meld calling', context=final_results)

        return final_results[0]
    def test_call_meld(self):
        client = Client()

        client.table.init_round(0, 0, 0, 0, 0, [0, 0, 0, 0])
        self.assertEqual(client.table.count_of_remaining_tiles, 70)

        meld = Meld()
        client.table.add_called_meld(0, meld)

        self.assertEqual(len(client.player.melds), 1)
        self.assertEqual(client.table.count_of_remaining_tiles, 71)

        client.player.tiles = [0]
        meld = Meld()
        meld.type = Meld.KAN
        meld.called_tile = 0
        client.table.add_called_meld(0, meld)

        self.assertEqual(len(client.player.melds), 2)
        # +1 for called meld
        # -1 for called kan
        self.assertEqual(client.table.count_of_remaining_tiles, 71)
Beispiel #6
0
    def test_closed_kan_and_riichi(self):
        table = Table()
        table.count_of_remaining_tiles = 60
        player = table.player
        player.scores = 25000

        kan_tiles = self._string_to_136_array(pin='7777')
        tiles = self._string_to_136_array(pin='568',
                                          sou='1235788') + kan_tiles[:3]
        player.init_hand(tiles)

        # +3 to avoid tile duplication of 7 pin
        tile = kan_tiles[3]
        player.draw_tile(tile)

        kan_type = player.should_call_kan(tile, False)
        self.assertEqual(kan_type, Meld.KAN)

        meld = Meld()
        meld.type = Meld.KAN
        meld.tiles = kan_tiles
        meld.called_tile = tile
        meld.who = 0
        meld.from_who = 0
        meld.opened = False

        # replacement from the dead wall
        player.draw_tile(self._string_to_136_tile(pin='4'))
        table.add_called_meld(meld.who, meld)
        discard = player.discard_tile()

        self.assertEqual(self._to_string([discard]), '8p')
        self.assertEqual(player.can_call_riichi(), True)

        # with closed kan we can't call riichi
        player.melds[0].opened = True
        self.assertEqual(player.can_call_riichi(), False)
Beispiel #7
0
    def test_call_meld_kan_from_player(self):
        client = Client()

        client.table.init_round(0, 0, 0, 0, 0, [0, 0, 0, 0])
        self.assertEqual(client.table.count_of_remaining_tiles, 70)

        meld = Meld()
        client.table.add_called_meld(0, meld)

        self.assertEqual(len(client.player.melds), 1)
        self.assertEqual(client.table.count_of_remaining_tiles, 71)

        client.player.tiles = [0]
        meld = Meld()
        meld.type = Meld.KAN
        # closed kan
        meld.tiles = [0, 1, 2, 3]
        meld.called_tile = 0
        meld.opened = True
        client.table.add_called_meld(0, meld)

        self.assertEqual(len(client.player.melds), 2)
        # kan was called from another player, total number of remaining tiles stays the same
        self.assertEqual(client.table.count_of_remaining_tiles, 71)
Beispiel #8
0
    def test_call_meld_closed_kan(self):
        client = Client()

        client.table.init_round(0, 0, 0, 0, 0, [0, 0, 0, 0])
        self.assertEqual(client.table.count_of_remaining_tiles, 70)

        meld = Meld()
        client.table.add_called_meld(0, meld)

        self.assertEqual(len(client.player.melds), 1)
        self.assertEqual(client.table.count_of_remaining_tiles, 71)

        client.player.tiles = [0]
        meld = Meld()
        meld.type = Meld.KAN
        # closed kan
        meld.tiles = [0, 1, 2, 3]
        meld.called_tile = None
        meld.opened = False
        client.table.add_called_meld(0, meld)

        self.assertEqual(len(client.player.melds), 2)
        # kan was closed, so -1
        self.assertEqual(client.table.count_of_remaining_tiles, 70)
Beispiel #9
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
Beispiel #10
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