def test_chose_strategy_and_reset_strategy(self): table = Table() table.has_open_tanyao = True player = table.player tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3') player.init_hand(tiles) self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO) # we draw a tile that will change our selected strategy tile = self._string_to_136_tile(sou='8') player.draw_tile(tile) self.assertEqual(player.ai.current_strategy, None) tiles = self._string_to_136_array(man='33355788', sou='3479', honors='3') player.init_hand(tiles) self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO) # for already opened hand we don't need to give up on selected strategy meld = Meld() meld.tiles = [1, 2, 3] player.add_called_meld(meld) tile = self._string_to_136_tile(sou='8') player.draw_tile(tile) self.assertEqual(player.ai.current_strategy.type, BaseStrategy.TANYAO)
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
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
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_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)
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)
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)
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): """ 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