def test_second_one_suit_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(sou='111123666789', honors='11') result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ['111s', '123s', '666s', '789s', '11z'])
def test_second_one_suit_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(sou="111123666789", honors="11") result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ["111s", "123s", "666s", "789s", "11z"])
def test_hand_with_pairs_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man='23444', pin='344556', sou='333') result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual( self._string(result[0]), ['234m', '44m', '345p', '456p', '333s'] )
def test_simple_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man="234567", sou="23455", honors="777") result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ["234m", "567m", "234s", "55s", "777z"])
def test_hand_with_pairs_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man="23444", pin="344556", sou="333") result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ["234m", "44m", "345p", "456p", "333s"])
def test_one_suit_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man="11122233388899") result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 2) self.assertEqual(self._string(result[0]), ["111m", "222m", "333m", "888m", "99m"]) self.assertEqual(self._string(result[1]), ["123m", "123m", "123m", "888m", "99m"])
def test_one_suit_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man='11122233388899') result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 2) self.assertEqual(self._string(result[0]), ['111m', '222m', '333m', '888m', '99m']) self.assertEqual(self._string(result[1]), ['123m', '123m', '123m', '888m', '99m'])
def test_second_simple_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man='123', pin='123', sou='123', honors='11222') result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual( self._string(result[0]), ['123m', '123p', '123s', '11z', '222z'] )
def test_simple_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man='234567', sou='23455', honors='777') result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual( self._string(result[0]), ['234m', '567m', '234s', '55s', '777z'] )
def test_second_simple_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man="123", pin="123", sou="123", honors="11222") result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ["123m", "123p", "123s", "11z", "222z"])
def test_chitoitsu_like_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man='112233', pin='99', sou='445566') result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 2) self.assertEqual(self._string(result[0]), ['11m', '22m', '33m', '99p', '44s', '55s', '66s']) self.assertEqual(self._string(result[1]), ['123m', '123m', '99p', '456s', '456s'])
def test_third_one_suit_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(pin='234777888999', honors='22') melds = [ self._make_meld(Meld.CHI, pin='789'), self._make_meld(Meld.CHI, pin='234'), ] result = hand.divide_hand(tiles_34, melds) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ['234p', '789p', '789p', '789p', '22z'])
def test_third_one_suit_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(pin="234777888999", honors="22") melds = [ self._make_meld(Meld.CHI, pin="789"), self._make_meld(Meld.CHI, pin="234"), ] result = hand.divide_hand(tiles_34, melds) self.assertEqual(len(result), 1) self.assertEqual(self._string(result[0]), ["234p", "789p", "789p", "789p", "22z"])
def test_chitoitsu_like_hand_dividing(self): hand = HandDivider() tiles_34 = self._string_to_34_array(man="112233", pin="99", sou="445566") result = hand.divide_hand(tiles_34) self.assertEqual(len(result), 2) self.assertEqual(self._string(result[0]), ["11m", "22m", "33m", "99p", "44s", "55s", "66s"]) self.assertEqual(self._string(result[1]), ["123m", "123m", "99p", "456s", "456s"])
def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() self.shanten = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.previous_shanten = 7 self.current_strategy = None self.waiting = [] self.in_defence = False self.last_discard_option = None
def __init__(self, player): self.player = player self.table = player.table self.chi = Chi(player) self.pon = Pon(player) self.kan = Kan(player) self.riichi = Riichi(player) self.discard = Discard(player) self.grp = GlobalRewardPredictor() self.hand_builder = HandBuilder(player, self) self.shanten_calculator = Shanten() self.hand_cache_shanten = {} self.placement = player.config.PLACEMENT_HANDLER_CLASS(player) self.finished_hand = HandCalculator() self.hand_divider = HandDivider() self.erase_state()
def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() self.shanten_calculator = Shanten() self.defence = DefenceHandler(player) self.riichi = Riichi(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.hand_builder = HandBuilder(player, self) self.erase_state()
def __init__(self, player): self.player = player self.table = player.table self.kan = Kan(player) self.agari = Agari() self.shanten_calculator = Shanten() self.defence = TileDangerHandler(player) self.riichi = Riichi(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.hand_builder = HandBuilder(player, self) self.placement = player.config.PLACEMENT_HANDLER_CLASS(player) self.suji = Suji(player) self.kabe = Kabe(player) self.erase_state()
class Phoenix: def __init__(self, player): self.player = player self.table = player.table self.chi = Chi(player) self.pon = Pon(player) self.kan = Kan(player) self.riichi = Riichi(player) self.discard = Discard(player) self.grp = GlobalRewardPredictor() self.hand_builder = HandBuilder(player, self) self.shanten_calculator = Shanten() self.hand_cache_shanten = {} self.placement = player.config.PLACEMENT_HANDLER_CLASS(player) self.finished_hand = HandCalculator() self.hand_divider = HandDivider() self.erase_state() def erase_state(self): self.hand_cache_shanten = {} self.hand_cache_estimation = {} self.finished_hand = HandCalculator() self.grp_features = [] def collect_experience(self): #collect round info init_scores = np.array(self.table.init_scores) / 1e5 gains = np.array(self.table.gains) / 1e5 dans = np.array( [RANKS.index(p.rank) for p in self.player.table.players]) dealer = int(self.player.dealer_seat) repeat_dealer = self.player.table.count_of_honba_sticks riichi_bets = self.player.table.count_of_riichi_sticks features = np.concatenate( (init_scores, gains, dans, np.array([dealer, repeat_dealer, riichi_bets])), axis=0) self.grp_features.append(features) #prepare input grp_input = [np.zeros(pred_emb_dim)] * max( round_num - len(self.grp_features), 0) + self.grp_features[:] reward = self.grp.get_global_reward( np.expand_dims(np.asarray(grp_input), axis=0))[0][self.player.seat] for model in [self.chi, self.pon, self.kan, self.riichi, self.discard]: model.collector.complete_episode(reward) def write_buffer(self): for model in [self.chi, self.pon, self.kan, self.riichi, self.discard]: model.collector.to_buffer() def init_hand(self): self.player.logger.debug( log.INIT_HAND, context=[ f"Round wind: {DISPLAY_WINDS[self.table.round_wind_tile]}", f"Player wind: {DISPLAY_WINDS[self.player.player_wind]}", f"Hand: {self.player.format_hand_for_print()}", ], ) self.shanten, _ = self.hand_builder.calculate_shanten_and_decide_hand_structure( TilesConverter.to_34_array(self.player.tiles)) def draw_tile(self): pass def discard_tile(self, discard_tile): ''' return discarded_tile and with_riichi ''' if discard_tile is not None: #discard after meld return discard_tile, False if self.player.is_open_hand: #can not riichi return self.discard.discard_tile(), False shanten = self.calculate_shanten_or_get_from_cache( TilesConverter.to_34_array(self.player.closed_hand)) if shanten != 0: #can not riichi return self.discard.discard_tile(), False with_riichi, p = self.riichi.should_call_riichi() if with_riichi: # fix here: might need review riichi_options = [ tile for tile in self.player.closed_hand if self.calculate_shanten_or_get_from_cache( TilesConverter.to_34_array( [t for t in self.player.closed_hand if t != tile])) == 0 ] tile_to_discard = self.discard.discard_tile( with_riichi_options=riichi_options) else: tile_to_discard = self.discard.discard_tile() return tile_to_discard, with_riichi def try_to_call_meld(self, tile_136, is_kamicha_discard, meld_type): # 1 pon # 2 kan (it is a closed kan and can be send only to the self draw) # 4 chi # there is two return value, meldPrint() and discardOption(), # while the second would not be used by client.py meld_chi, meld_pon = None, None should_chi, should_pon = False, False # print(tile_136) # print(self.player.closed_hand) melds_chi, melds_pon = self.get_possible_meld(tile_136, is_kamicha_discard) if melds_chi and meld_type & 4: should_chi, chi_score, tiles_chi = self.chi.should_call_chi( tile_136, melds_chi) # fix here: tiles_chi is now the first possible meld ---fixed! # tiles_chi = melds_chi[0] meld_chi = Meld(meld_type="chi", tiles=tiles_chi) if meld_chi else None if melds_pon and meld_type & 1: should_pon, pon_score = self.pon.should_call_pon( tile_136, is_kamicha_discard) tiles_pon = melds_pon[0] meld_pon = Meld(meld_type="pon", tiles=tiles_pon) if meld_pon else None if not should_chi and not should_pon: return None, None if should_chi and should_pon: meld = meld_chi if chi_score > pon_score else meld_pon elif should_chi: meld = meld_chi else: meld = meld_pon all_tiles_copy, meld_tiles_copy = self.player.tiles[:], self.player.meld_tiles[:] all_tiles_copy.append(tile_136) meld_tiles_copy.append(meld) closed_hand_copy = [ item for item in all_tiles_copy if item not in meld_tiles_copy ] discard_option = self.discard.discard_tile( all_hands_136=all_tiles_copy, closed_hands_136=closed_hand_copy) return meld, discard_option def should_call_kyuushu_kyuuhai(self): #try kokushi strategy if with 10 types tiles_34 = TilesConverter.to_34_array(self.player.tiles) types = sum([1 for t in tiles_34 if t > 0]) if types >= 10: return False else: return True def should_call_win(self, tile, is_tsumo, enemy_seat=None, is_chankan=False): # don't skip win in riichi if self.player.in_riichi: return True # currently we don't support win skipping for tsumo if is_tsumo: return True # fast path - check it first to not calculate hand cost cost_needed = self.placement.get_minimal_cost_needed() if cost_needed == 0: return True # 1 and not 0 because we call check for win this before updating remaining tiles is_hotei = self.player.table.count_of_remaining_tiles == 1 hand_response = self.calculate_exact_hand_value_or_get_from_cache( tile, tiles=self.player.tiles, call_riichi=self.player.in_riichi, is_tsumo=is_tsumo, is_chankan=is_chankan, is_haitei=is_hotei, ) assert hand_response is not None assert not hand_response.error, hand_response.error cost = hand_response.cost return self.placement.should_call_win(cost, is_tsumo, enemy_seat) def calculate_shanten_or_get_from_cache(self, closed_hand_34: List[int], use_chiitoitsu=True): """ Sometimes we are calculating shanten for the same hand multiple times to save some resources let's cache previous calculations """ key = build_shanten_cache_key(closed_hand_34, use_chiitoitsu) if key in self.hand_cache_shanten: return self.hand_cache_shanten[key] # if use_chiitoitsu and not self.player.is_open_hand: # result = self.shanten_calculator.calculate_shanten_for_chiitoitsu_hand(closed_hand_34) # else: # result = self.shanten_calculator.calculate_shanten_for_regular_hand(closed_hand_34) # fix here: a little bit strange in use_chiitoitsu shanten_results = [] if use_chiitoitsu and not self.player.is_open_hand: shanten_results.append( self.shanten_calculator.calculate_shanten_for_chiitoitsu_hand( closed_hand_34)) shanten_results.append( self.shanten_calculator.calculate_shanten_for_regular_hand( closed_hand_34)) result = min(shanten_results) self.hand_cache_shanten[key] = result return result def calculate_exact_hand_value_or_get_from_cache( self, win_tile_136, tiles=None, call_riichi=False, is_tsumo=False, is_chankan=False, is_haitei=False, is_ippatsu=False, ): if not tiles: tiles = self.player.tiles[:] else: tiles = tiles[:] if win_tile_136 not in tiles: tiles += [win_tile_136] additional_han = 0 if is_chankan: additional_han += 1 if is_haitei: additional_han += 1 if is_ippatsu: additional_han += 1 config = HandConfig( is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind_tile, is_tsumo=is_tsumo, options=OptionalRules( has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao, has_double_yakuman=False, ), is_chankan=is_chankan, is_ippatsu=is_ippatsu, is_haitei=is_tsumo and is_haitei or False, is_houtei=(not is_tsumo) and is_haitei or False, tsumi_number=self.player.table.count_of_honba_sticks, kyoutaku_number=self.player.table.count_of_riichi_sticks, ) return self._estimate_hand_value_or_get_from_cache( win_tile_136, tiles, call_riichi, is_tsumo, additional_han, config) def _estimate_hand_value_or_get_from_cache(self, win_tile_136, tiles, call_riichi, is_tsumo, additional_han, config, is_rinshan=False, is_chankan=False): cache_key = build_estimate_hand_value_cache_key( tiles, call_riichi, is_tsumo, self.player.melds, self.player.table.dora_indicators, self.player.table.count_of_riichi_sticks, self.player.table.count_of_honba_sticks, additional_han, is_rinshan, is_chankan, ) if self.hand_cache_estimation.get(cache_key): return self.hand_cache_estimation.get(cache_key) result = self.finished_hand.estimate_hand_value( tiles, win_tile_136, self.player.melds, self.player.table.dora_indicators, config, use_hand_divider_cache=True, ) self.hand_cache_estimation[cache_key] = result return result @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:] def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ pass 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 def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ pass
class ImplementationAI(InterfaceAI): version = '0.0.1' agari = None shanten = None defence = None hand_divider = None finished_hand = None last_discard_option = None previous_shanten = 7 in_defence = False waiting = None current_strategy = None def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() self.shanten = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.previous_shanten = 7 self.current_strategy = None self.waiting = [] self.in_defence = False self.last_discard_option = None def init_hand(self): """ Let's decide what we will do with our hand (like open for tanyao and etc.) """ self.determine_strategy() def erase_state(self): self.current_strategy = None self.in_defence = False self.last_discard_option = None def draw_tile(self, tile): """ :param tile: 136 tile format :return: """ self.determine_strategy() def discard_tile(self, discard_tile): # we called meld and we had discard tile that we wanted to discard if discard_tile is not None: if not self.last_discard_option: return discard_tile return self.process_discard_option(self.last_discard_option, self.player.closed_hand, True) results, shanten = self.calculate_outs(self.player.tiles, self.player.closed_hand, self.player.open_hand_34_tiles) selected_tile = self.process_discard_options_and_select_tile_to_discard( results, shanten) # bot think that there is a threat on the table # and better to fold # if we can't find safe tiles, let's continue to build our hand if self.defence.should_go_to_defence_mode(selected_tile): if not self.in_defence: logger.info('We decided to fold against other players') self.in_defence = True defence_tile = self.defence.try_to_find_safe_tile_to_discard( results) if defence_tile: return self.process_discard_option(defence_tile, self.player.closed_hand) else: self.in_defence = False return self.process_discard_option(selected_tile, self.player.closed_hand) def process_discard_options_and_select_tile_to_discard( self, results, shanten, had_was_open=False): tiles_34 = TilesConverter.to_34_array(self.player.tiles) # we had to update tiles value there # because it is related with shanten number for result in results: result.tiles_count = self.count_tiles(result.waiting, tiles_34) result.calculate_value(shanten) # current strategy can affect on our discard options # so, don't use strategy specific choices for calling riichi if self.current_strategy: results = self.current_strategy.determine_what_to_discard( self.player.closed_hand, results, shanten, False, None, had_was_open) return self.chose_tile_to_discard(results) def calculate_outs(self, tiles, closed_hand, open_sets_34=None): """ :param tiles: array of tiles in 136 format :param closed_hand: array of tiles in 136 format :param open_sets_34: array of array with tiles in 34 format :return: """ tiles_34 = TilesConverter.to_34_array(tiles) closed_tiles_34 = TilesConverter.to_34_array(closed_hand) is_agari = self.agari.is_agari(tiles_34, self.player.open_hand_34_tiles) results = [] for hand_tile in range(0, 34): if not closed_tiles_34[hand_tile]: continue tiles_34[hand_tile] -= 1 shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) waiting = [] for j in range(0, 34): if hand_tile == j or tiles_34[j] == 4: continue tiles_34[j] += 1 if self.shanten.calculate_shanten(tiles_34, open_sets_34) == shanten - 1: waiting.append(j) tiles_34[j] -= 1 tiles_34[hand_tile] += 1 if waiting: results.append( DiscardOption(player=self.player, shanten=shanten, tile_to_discard=hand_tile, waiting=waiting, tiles_count=self.count_tiles( waiting, tiles_34))) if is_agari: shanten = Shanten.AGARI_STATE else: shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) return results, shanten def count_tiles(self, waiting, tiles_34): n = 0 for item in waiting: n += 4 - self.player.total_tiles(item, tiles_34) return n def try_to_call_meld(self, tile, is_kamicha_discard): if not self.current_strategy: return None, None if len(self.player.discards) <= 5 and tile // 4 <= 27: return None, None meld, discard_option = self.current_strategy.try_to_call_meld( tile, is_kamicha_discard) tile_to_discard = None if discard_option: self.last_discard_option = discard_option tile_to_discard = discard_option.tile_to_discard return meld, tile_to_discard def determine_strategy(self): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False return False old_strategy = self.current_strategy self.current_strategy = None # order is important, the first appropriate strtegy will be used strategies = [] if self.player.table.has_open_tanyao: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) strategies.append(YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player)) strategies.append(HonitsuStrategy(BaseStrategy.HONITSU, self.player)) for strategy in strategies: if strategy.should_activate_strategy(): self.current_strategy = strategy if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: message = '{} switched to {} strategy'.format( self.player.name, self.current_strategy) if old_strategy: message += ' from {}'.format(old_strategy) logger.debug(message) logger.debug('With hand: {}'.format( TilesConverter.to_one_line_string(self.player.tiles))) if not self.current_strategy and old_strategy: logger.debug('{} gave up on {}'.format(self.player.name, old_strategy)) return self.current_strategy and True or False def chose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: """ Try to find best tile to discard, based on different valuations """ def sorting(x): # - is important for x.tiles_count # in that case we will discard tile that will give for us more tiles # to complete a hand return x.shanten, -x.tiles_count, x.valuation had_to_be_discarded_tiles = [ x for x in results if x.had_to_be_discarded ] if had_to_be_discarded_tiles: had_to_be_discarded_tiles = sorted(had_to_be_discarded_tiles, key=sorting) selected_tile = had_to_be_discarded_tiles[0] else: results = sorted(results, key=sorting) # remove needed tiles from discard options results = [x for x in results if not x.had_to_be_saved] # let's chose most valuable tile first temp_tile = results[0] # and let's find all tiles with same shanten results_with_same_shanten = [ x for x in results if x.shanten == temp_tile.shanten ] possible_options = [temp_tile] for discard_option in results_with_same_shanten: # there is no sense to check already chosen tile if discard_option.tile_to_discard == temp_tile.tile_to_discard: continue # we don't need to select tiles almost dead waits if discard_option.tiles_count <= 2: continue # let's check all other tiles with same shanten # maybe we can find tiles that have almost same tiles count number if temp_tile.tiles_count - 2 < discard_option.tiles_count < temp_tile.tiles_count + 2: possible_options.append(discard_option) # let's sort got tiles by value and let's chose less valuable tile to discard possible_options = sorted(possible_options, key=lambda x: x.valuation) selected_tile = possible_options[0] return selected_tile def process_discard_option(self, discard_option, closed_hand, force_discard=False): self.waiting = discard_option.waiting self.player.ai.previous_shanten = discard_option.shanten self.player.in_tempai = self.player.ai.previous_shanten == 0 # when we called meld we don't need "smart" discard if force_discard: return discard_option.find_tile_in_hand(closed_hand) last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard: return self.player.last_draw else: return discard_option.find_tile_in_hand(closed_hand) def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): """ :param win_tile: 34 tile format :param tiles: :param call_riichi: :return: """ win_tile *= 4 # we don't need to think, that our waiting is aka dora if win_tile in AKA_DORA_LIST: win_tile += 1 if not tiles: tiles = self.player.tiles tiles += [win_tile] config = HandConfig(is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind, has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao) result = self.finished_hand.estimate_hand_value( tiles, win_tile, self.player.melds, self.player.table.dora_indicators, config) return result def should_call_riichi(self): print(self.player.discards) # empty waiting can be found in some cases if not self.waiting: return False if self.in_defence: return False #If we tenpai fast enough if len(self.player.discards) <= 8: return True if len(self.player.discards) >= 14: return False # we have a good wait, let's riichi if len(self.waiting) > 1: return True waiting = self.waiting[0] tiles = self.player.closed_hand + [waiting * 4] closed_melds = [x for x in self.player.melds if not x.opened] for meld in closed_melds: tiles.extend(meld.tiles[:3]) tiles_34 = TilesConverter.to_34_array(tiles) results = self.hand_divider.divide_hand(tiles_34) result = results[0] count_of_pairs = len([x for x in result if is_pair(x)]) # with chitoitsu we can call a riichi with pair wait if count_of_pairs == 7: return True for hand_set in result: # better to not call a riichi for a pair wait # it can be easily improved if is_pair(hand_set) and waiting in hand_set: return False return True def should_call_kan(self, tile, open_kan): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :return: kan type """ # we don't need to add dora for other players if self.player.ai.in_defence: return None if open_kan: # we don't want to start open our hand from called kan if not self.player.is_open_hand: return None # there is no sense to call open kan when we are not in tempai if not self.player.in_tempai: return None # we have a bad wait, rinshan chance is low if len(self.waiting) < 2: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # let's check can we upgrade opened pon to the kan if pon_melds: for meld in pon_melds: # tile is equal to our already opened pon, # so let's call chankan! if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 # we have 3 tiles in our hand, # so we can try to call closed meld if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # called kan will not ruin our hand if new_shanten <= previous_shanten: return Meld.KAN return None def should_call_win(self, tile, enemy_seat): return True def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ if self.defence.should_go_to_defence_mode(): self.in_defence = True @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:]
def _hand(self, tiles, hand_index=0): hand_divider = HandDivider() return hand_divider.divide_hand(tiles)[hand_index]
class ImplementationAI(InterfaceAI): version = '0.3.2' agari = None shanten = None defence = None hand_divider = None finished_hand = None last_discard_option = None previous_shanten = 7 in_defence = False waiting = None current_strategy = None def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() self.shanten = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.previous_shanten = 7 self.current_strategy = None self.waiting = [] self.in_defence = False self.last_discard_option = None # Added for cowboy self.wanted_tiles_count = 0 self.pushing = False def init_hand(self): """ Let's decide what we will do with our hand (like open for tanyao and etc.) """ self.determine_strategy() def erase_state(self): self.current_strategy = None self.in_defence = False self.last_discard_option = None # Added for cowboy self.previous_shanten = 7 self.pushing = False def draw_tile(self, tile): """ :param tile: 136 tile format :return: """ self.determine_strategy() def discard_tile(self, discard_tile): # we called meld and we had discard tile that we wanted to discard if discard_tile is not None: if not self.last_discard_option: return discard_tile return self.process_discard_option(self.last_discard_option, self.player.closed_hand, True) results, shanten = self.calculate_outs(self.player.tiles, self.player.closed_hand, self.player.open_hand_34_tiles) if shanten < self.previous_shanten: logger.info("Shanten: {}".format(shanten)) self.previous_shanten = shanten selected_tile = self.process_discard_options_and_select_tile_to_discard(results, shanten) # bot think that there is a threat on the table # and better to fold # if we can't find safe tiles, let's continue to build our hand if self.defence.should_go_to_defence_mode(selected_tile): if not self.in_defence: logger.info('We decided to fold against other players') self.in_defence = True self.player.set_state("DEFENCE") else: #logger.info("Player is alreay in defence") pass defence_results, shanten = self.calculate_outs(self.player.tiles, self.player.closed_hand, self.player.open_hand_34_tiles) defence_tile = self.defence.try_to_find_safe_tile_to_discard(defence_results) if defence_tile: return self.process_discard_option(defence_tile, self.player.closed_hand) else: self.in_defence = False # Process the discard option before changing the state card2discard = self.process_discard_option(selected_tile, self.player.closed_hand) # After adjusting the defence, time to update the state if shanten == 0 and self.player.play_state == "PREPARING" and results: # and results for debugging if self.wanted_tiles_count > 4: self.player.set_state("PROACTIVE_GOODSHAPE") else: self.player.set_state("PROACTIVE_BADSHAPE") return card2discard def process_discard_options_and_select_tile_to_discard(self, results, shanten, had_was_open=False): tiles_34 = TilesConverter.to_34_array(self.player.tiles) # we had to update tiles value there # because it is related with shanten number for result in results: result.tiles_count = self.count_tiles(result.waiting, tiles_34) result.calculate_value(shanten) # current strategy can affect on our discard options # so, don't use strategy specific choices for calling riichi if self.current_strategy: results = self.current_strategy.determine_what_to_discard(self.player.closed_hand, results, shanten, False, None, had_was_open) return self.chose_tile_to_discard(results) def calculate_outs(self, tiles, closed_hand, open_sets_34=None): """ :param tiles: array of tiles in 136 format :param closed_hand: array of tiles in 136 format :param open_sets_34: array of array with tiles in 34 format :return: """ tiles_34 = TilesConverter.to_34_array(tiles) closed_tiles_34 = TilesConverter.to_34_array(closed_hand) is_agari = self.agari.is_agari(tiles_34, self.player.open_hand_34_tiles) results = [] for hand_tile in range(0, 34): if not closed_tiles_34[hand_tile]: continue tiles_34[hand_tile] -= 1 shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) waiting = [] for j in range(0, 34): if hand_tile == j or tiles_34[j] == 4: continue tiles_34[j] += 1 if self.shanten.calculate_shanten(tiles_34, open_sets_34) == shanten - 1: waiting.append(j) tiles_34[j] -= 1 tiles_34[hand_tile] += 1 if waiting: results.append(DiscardOption(player=self.player, shanten=shanten, tile_to_discard=hand_tile, waiting=waiting, tiles_count=self.count_tiles(waiting, tiles_34))) if is_agari: shanten = Shanten.AGARI_STATE else: shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) return results, shanten def count_tiles(self, waiting, tiles_34): n = 0 for item in waiting: n += 4 - self.player.total_tiles(item, tiles_34) return n def try_to_call_meld(self, tile, is_kamicha_discard): if not self.current_strategy: return None, None meld, discard_option = self.current_strategy.try_to_call_meld(tile, is_kamicha_discard) tile_to_discard = None if discard_option: self.last_discard_option = discard_option tile_to_discard = discard_option.tile_to_discard return meld, tile_to_discard def determine_strategy(self): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False old_strategy = self.current_strategy self.current_strategy = None # order is important strategies = [ YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player), HonitsuStrategy(BaseStrategy.HONITSU, self.player), ] if self.player.table.has_open_tanyao: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) for strategy in strategies: if strategy.should_activate_strategy(): self.current_strategy = strategy if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: message = '{} switched to {} strategy'.format(self.player.name, self.current_strategy) if old_strategy: message += ' from {}'.format(old_strategy) logger.info(message) logger.info('With such a hand: {}'.format(TilesConverter.to_one_line_string(self.player.tiles))) if not self.current_strategy and old_strategy: logger.debug('{} gave up on {}'.format(self.player.name, old_strategy)) return self.current_strategy and True or False def chose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: """ Try to find best tile to discard, based on different valuations """ def sorting(x): # - is important for x.tiles_count # in that case we will discard tile that will give for us more tiles # to complete a hand return x.shanten, -x.tiles_count, x.valuation # util for drawing def get_order(t): # if it is honor if t // 9 >= 3: return 0 else: return min((t % 9), (8 - (t % 9))) + 1 def display_waiting(w): return TilesConverter.to_one_line_string([t * 4 for t in w]) had_to_be_discarded_tiles = [x for x in results if x.had_to_be_discarded] if had_to_be_discarded_tiles: had_to_be_discarded_tiles = sorted(had_to_be_discarded_tiles, key=sorting) selected_tile = had_to_be_discarded_tiles[0] else: results = sorted(results, key=sorting) #print("Len: ", len(results)) # init the temp_tile temp_tile = results[0] # remove needed tiles from discard options results = [x for x in results if not x.had_to_be_saved] # let's chose most valuable tile first if results: temp_tile = results[0] else: return temp_tile # and let's find all tiles with same shanten results_with_same_shanten = [x for x in results if x.shanten == temp_tile.shanten] # if there are 7 pairs tiles_34 = TilesConverter.to_34_array(self.player.tiles) paired_tiles = [x for x in range(0, 34) if tiles_34[x] == 2] num_pairs = len(paired_tiles) if num_pairs == 4 and temp_tile.shanten > 1 and not self.player.in_seven_pairs and not self.player.params.get("fool_in_pairs"): logger.info("There are 4 pairs!") if len(self.player.discards) > 6: logger.info("However it's too late for seven pairs.") for r in results: if r.tile_to_discard in paired_tiles: logger.info("With hand: {}".format(TilesConverter.to_one_line_string(self.player.tiles))) logger.info("Discard {}".format(display_waiting([r.tile_to_discard]))) return r else: logger.info("It's early, okay to go with seven pairs.") self.player.in_seven_pairs = True # TODO: a smart seven pairs strategy should be carried if self.player.in_seven_pairs and not self.player.params.get("fool_in_pairs"): single_tiles = [x for x in range(0,34) if tiles_34[x] in [1,3,4]] single_tiles.sort(key=lambda x: (self.count_tiles([x], tiles_34) >= 2, -get_order(x))) for s in single_tiles: # actually only #1 would be used most of the time for r in results: if r.tile_to_discard == s: logger.info("SevenPairsStrategy:") logger.info("Hand: {}".format(TilesConverter.to_one_line_string(self.player.tiles))) logger.info("Discard: {}".format(display_waiting([s]))) return r # if in drawing if temp_tile.shanten == 0: print("It's a drawing hand!") print("Hand: {}".format(TilesConverter.to_one_line_string(self.player.tiles))) # assume that temp tile got the biggest waiting if temp_tile.tiles_count > 4: print("It's a good shape, go for it.") else: logger.info("It's a bad shape drawing hand, need some calculation.") logger.info("Possible choices: {}".format(TilesConverter.to_one_line_string([x.tile_to_discard*4 for x in results_with_same_shanten]))) possible_choices = [(temp_tile, 99)] for r in results_with_same_shanten: print("\nCut:", display_waiting([r.tile_to_discard])) print("Waiting:", display_waiting(r.waiting)) print("Order:", [get_order(t) for t in r.waiting]) print("Outs:", r.tiles_count) if r.tiles_count == 0: print("It's an impossible drawing.") continue if len(r.waiting) == 1: print("It's an 1 out drawing.") possible_choices.append((r, get_order(r.waiting[0]))) else: print("It's a multiple out drawing.") r.waiting.sort(key=get_order) possible_choices.append((r, get_order(r.waiting[0]))) possible_choices.sort(key=lambda x: (x[1], -x[0].tiles_count)) final_choice = possible_choices[0][0] logger.info("Choice: {} {} {}".format(display_waiting([final_choice.tile_to_discard]), "with waiting", display_waiting(final_choice.waiting))) return final_choice # if not in drawing or in drawing with good shape possible_options = [temp_tile] for discard_option in results_with_same_shanten: # there is no sense to check already chosen tile if discard_option.tile_to_discard == temp_tile.tile_to_discard: continue # we don't need to select tiles almost dead waits if discard_option.tiles_count <= 2: continue # let's check all other tiles with same shanten # maybe we can find tiles that have almost same tiles count number # Cowboy: +-2 is a big difference, but +-1 is not diff = 1 if self.player.params.get("big_diff"): diff = 2 if temp_tile.tiles_count - diff < discard_option.tiles_count < temp_tile.tiles_count + diff: possible_options.append(discard_option) # let's sort got tiles by value and let's chose less valuable tile to discard possible_options = sorted(possible_options, key=lambda x: x.valuation) selected_tile = possible_options[0] if selected_tile.shanten == 0: print("\nChoice:", display_waiting([selected_tile.tile_to_discard]), "with waiting", display_waiting(selected_tile.waiting)) return selected_tile def process_discard_option(self, discard_option, closed_hand, force_discard=False): self.waiting = discard_option.waiting self.wanted_tiles_count = discard_option.tiles_count self.player.ai.previous_shanten = discard_option.shanten self.player.in_tempai = self.player.ai.previous_shanten == 0 # when we called meld we don't need "smart" discard if force_discard: return discard_option.find_tile_in_hand(closed_hand) last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard: return self.player.last_draw else: return discard_option.find_tile_in_hand(closed_hand) def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): """ :param win_tile: 34 tile format :param tiles: :param call_riichi: :return: """ win_tile *= 4 # we don't need to think, that our waiting is aka dora if win_tile in AKA_DORA_LIST: win_tile += 1 if not tiles: tiles = self.player.tiles tiles += [win_tile] config = HandConfig( is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind, has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao ) result = self.finished_hand.estimate_hand_value(tiles, win_tile, self.player.melds, self.player.table.dora_indicators, config) return result def should_call_riichi(self): logger.info("Can call a reach!") # empty waiting can be found in some cases if not self.waiting: logger.info("However it is impossible to win.") return False # In pushing state, it's better to call it if self.pushing: logger.info("Go for it! The player is in pushing state.") return True # Get the rank EV after round 3 if self.table.round_number >= 5: # DEBUG: set this to 0 try: possible_hand_values = [self.estimate_hand_value(tile, call_riichi=True).cost["main"] for tile in self.waiting] except Exception as e: print(e) possible_hand_values = [2000] hand_value = sum(possible_hand_values) / len(possible_hand_values) hand_value += self.table.count_of_riichi_sticks * 1000 if self.player.is_dealer: hand_value += 700 # EV for dealer combo lose_estimation = 6000 if self.player.is_dealer else 7000 hand_shape = "pro_bad_shape" if self.wanted_tiles_count <= 4 else "pro_good_shape" rank_ev = self.defence.get_rank_ev(hand_value, lose_estimation, COUNTER_RATIO[hand_shape][len(self.player.discards)]) logger.info('''Cowboy: Proactive reach: Hand value: {} Hand shape: {} Is dealer: {} Current ranking: {} '''.format(hand_value, hand_shape, self.player.is_dealer, self.table.get_players_sorted_by_scores())) logger.info("Rank EV for proactive reach: {}".format(rank_ev)) if rank_ev < 0: logger.info("It's better to fold.") return False else: logger.info("Go for it!") return True should_attack = not self.defence.should_go_to_defence_mode() # For bad shape, at least 1 dora is required # Get count of dora dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles]) # aka dora dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_open_tanyao)]) if self.wanted_tiles_count <= 4 and dora_count == 0 and not self.player.is_dealer: should_attack = False logger.info("A bad shape with no dora, don't call it.") # # If player is on the top, no need to call reach # if self.player == self.player.table.get_players_sorted_by_scores()[0] and self.player.scores > 30000: # should_attack = False # logger.info("Player is in 1st position, no need to call reach.") if should_attack: # If we are proactive, let's set the state! logger.info("Go for it!") if self.player.play_state == "PREPARING": # If not changed in defense actions if self.wanted_tiles_count > 4: self.player.set_state("PROACTIVE_GOODSHAPE") else: self.player.set_state("PROACTIVE_BADSHAPE") return True else: logger.info("However it's better to fold.") return False # These codes are unreachable, it is fine. waiting = self.waiting[0] tiles = self.player.closed_hand + [waiting * 4] closed_melds = [x for x in self.player.melds if not x.opened] for meld in closed_melds: tiles.extend(meld.tiles[:3]) tiles_34 = TilesConverter.to_34_array(tiles) results = self.hand_divider.divide_hand(tiles_34) result = results[0] count_of_pairs = len([x for x in result if is_pair(x)]) # with chitoitsu we can call a riichi with pair wait if count_of_pairs == 7: return True for hand_set in result: # better to not call a riichi for a pair wait # it can be easily improved if is_pair(hand_set) and waiting in hand_set: return False return True def should_call_kan(self, tile, open_kan): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :return: kan type """ # we don't need to add dora for other players if self.player.ai.in_defence: return None if open_kan: # we don't want to start open our hand from called kan if not self.player.is_open_hand: return None # there is no sense to call open kan when we are not in tempai if not self.player.in_tempai: return None # we have a bad wait, rinshan chance is low if len(self.waiting) < 2: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # let's check can we upgrade opened pon to the kan if pon_melds: for meld in pon_melds: # tile is equal to our already opened pon, # so let's call chankan! if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 # we have 3 tiles in our hand, # so we can try to call closed meld if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # called kan will not ruin our hand if new_shanten <= previous_shanten: return Meld.KAN return None def should_call_win(self, tile, enemy_seat): return True def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ #if self.defence.should_go_to_defence_mode(): # self.in_defence = True # No need to check it here pass @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:]
class ImplementationAI(InterfaceAI): """ AI that will discard tiles to maximize expected shanten. Assumes that tiles are drawn randomly from those not on the table or in hand - aka not revealed to player. Does not account for hidden tiles in opponent's hands. Always calls wins, never calls riichi. TODO: Everything """ version = 'shantenMCS' shanten = None agari = None shdict = {} def __init__(self, player): super(ImplementationAI, self).__init__(player) self.shanten = Shanten() self.hand_divider = HandDivider() self.agari = Agari() self.iterations = 200 def simulate_single(self, hand, hand_open, unaccounted_tiles): """ #simulates a single random draw and calculates shanten hand, hand_open -- hand in 34 format unaccounted_tiles -- all the unused tiles in 34 format turn -- a number from 0-3 (0 is the player) """ hand = list(hand) unaccounted = list(unaccounted_tiles) #14 in dead wall 13*3= 39 in other hand -> total 53 unaccounted_nonzero = np.nonzero(unaccounted)[0] #get a random card draw_tile = random.choice(unaccounted_nonzero) unaccounted[draw_tile] -= 1 hand[draw_tile] += 1 return self.shanten.calculate_shanten(hand, hand_open) # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables def discard_tile(self, discard_tile): if discard_tile is not None: return discard_tile tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) results = [] for tile in range(0, 34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand tiles_34[tile] -= 1 # calculate shanten and store shanten = self.shanten.calculate_shanten( tiles_34, self.player.open_hand_34_tiles) results.append((shanten, tile)) # return tile to hand tiles_34[tile] += 1 (minshanten, discard_34) = min(results) results2 = [] unaccounted = (np.array([4]*34) - closed_tiles_34)\ - TilesConverter.to_34_array(self.table.revealed_tiles) self.shdict = {} for shanten, tile in results: if shanten != minshanten: continue h = sum( self.simulate_single(closed_tiles_34, self.player. open_hand_34_tiles, unaccounted) for _ in range(self.iterations)) / self.iterations results2.append((h, tile)) (h, discard_34) = min(results2) discard_136 = TilesConverter.find_34_tile_in_136_array( discard_34, self.player.closed_hand) if discard_136 is None: logger.debug('Failure') discard_136 = random.randrange(len(self.player.tiles) - 1) discard_136 = self.player.tiles[discard_136] logger.info('Shanten after discard:' + str(shanten)) logger.info('Discard heuristic:' + str(h)) return discard_136 # UNUSED def calculate_outs(self, discard_34, shanten, depth=2): closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) table_34 = list(self.table.revealed_tiles) tiles_34 = TilesConverter.to_34_array(self.player.tiles) table_34[discard_34] += 1 closed_tiles_34[discard_34] -= 1 tiles_34[discard_34] -= 1 hidden_34 = np.array( [4] * 34) - np.array(closed_tiles_34) - np.array(table_34) # print(hidden_34) # want to sample? use this # reveal_num = sum(hidden_34) # draw_p = [float(i)/reveal_num for i in hidden_34] # draw = np.random.choice(34, p=draw_p) return self.out_search(tiles_34, closed_tiles_34, hidden_34, depth, shanten - 1) def calculate_outs_meld(self, discard_34, shanten, tiles_34, closed_tiles_34, open_hand_34, depth=2): table_34 = list(self.table.revealed_tiles) tiles_34 = copy.deepcopy(tiles_34) closed_tiles_34 = copy.deepcopy(closed_tiles_34) table_34[discard_34] += 1 closed_tiles_34[discard_34] -= 1 tiles_34[discard_34] -= 1 hidden_34 = np.array( [4] * 34) - np.array(closed_tiles_34) - np.array(table_34) return self.out_search(tiles_34, closed_tiles_34, hidden_34, depth, shanten - 1, open_hand_34) def out_search(self, tiles_34, closed_tiles_34, hidden_34, depth, shanten, open_hand_34=None): outs = 0 for i in range(34): if hidden_34[i] <= 0: continue ct = hidden_34[i] # draw tile from hidden to concealed hand hidden_34[i] -= 1 closed_tiles_34[i] += 1 tiles_34[i] += 1 if self.agari.is_agari(tiles_34, open_hand_34): outs += 2 else: for tile in range(0, 34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand closed_tiles_34[tile] -= 1 tiles_34[tile] -= 1 tuple_34 = tuple(tiles_34) # calculate shanten and add outs if appropriate if tuple_34 in self.shdict.keys(): sh = self.shdict[tuple_34] else: if open_hand_34 is None: sh = self.shanten.calculate_shanten( tiles_34, self.player.open_hand_34_tiles) else: sh = self.shanten.calculate_shanten( tiles_34, open_hand_34) self.shdict[tuple_34] = sh if sh == shanten: if depth <= 1 or shanten == -1: outs += 1 else: outs += ct * self.out_search( tiles_34, closed_tiles_34, hidden_34, depth - 1, shanten - 1) if sh == shanten + 1: outs += 0.01 # return tile to hand closed_tiles_34[tile] += 1 tiles_34[tile] += 1 # return tile from closed hand to hidden hidden_34[i] += 1 closed_tiles_34[i] -= 1 tiles_34[i] -= 1 return outs def should_call_riichi(self): # if len(self.player.open_hand_34_tiles) != 0: # return False return True # tiles_34 = TilesConverter.to_34_array(self.player.tiles) # shanten = self.shanten.calculate_shanten(tiles_34, None) # logger.debug('Riichi check, shanten = ' + str(shanten)) # return shanten == 0 def should_call_win(self, tile, enemy_seat): return True def should_call_kan(self, tile, open_kan): """ When bot can call kan or chankan this method will be called :param tile: 136 tile format :param is_open_kan: boolean :return: kan type (Meld.KAN, Meld.CHANKAN) or None """ if open_kan: # don't start open hand from called kan if not self.player.is_open_hand: return None # don't call open kan if not waiting for win if not self.player.in_tempai: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # upgrade open pon to kan if possible if pon_melds: for meld in pon_melds: if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # check for improvement in shanten if new_shanten <= previous_shanten: return Meld.KAN return 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 # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables def meldDiscard(self, meld_34, discardtile): tiles_34 = TilesConverter.to_34_array(self.player.tiles + [discardtile]) closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand + [discardtile]) open_hand_34 = copy.deepcopy(self.player.open_hand_34_tiles) # remove meld from closed and and add to open hand open_hand_34.append(meld_34) for tile_34 in meld_34: closed_tiles_34[tile_34] -= 1 results = [] for tile in range(0, 34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand tiles_34[tile] -= 1 # calculate shanten and store shanten = self.shanten.calculate_shanten(tiles_34, open_hand_34) results.append((shanten, tile)) # return tile to hand tiles_34[tile] += 1 (minshanten, discard_34) = min(results) results2 = [] unaccounted = (np.array([4]*34) - closed_tiles_34)\ - TilesConverter.to_34_array(self.table.revealed_tiles) self.shdict = {} for shanten, tile in results: if shanten != minshanten: continue h = sum( self.simulate_single(closed_tiles_34, open_hand_34, unaccounted) for _ in range(self.iterations)) / self.iterations results2.append((h, tile)) (h, discard_34) = min(results2) discard_136 = TilesConverter.find_34_tile_in_136_array( discard_34, self.player.closed_hand) return minshanten, discard_136
class ImplementationAI(InterfaceAI): """ AI that will discard tiles as to minimize shanten, using perfect shanten calculation. Picks the first tile with resulting in the lowest shanten value when choosing what to discard. Calls riichi if possible and hand is closed. Always calls wins. Calls kan to upgrade pon or on equivalent or reduced shanten. Calls melds to reduce shanten. """ version = 'shantenNaive' shanten = None agari = None def __init__(self, player): super(ImplementationAI, self).__init__(player) self.shanten = Shanten() # self.agari = Agari() self.hand_divider = HandDivider() # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables def discard_tile(self, discard_tile): if discard_tile is not None: return discard_tile tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) # is_agari = self.agari.is_agari(tiles_34, self.player.open_hand_34_tiles) results = [] for tile in range(0,34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand tiles_34[tile] -= 1 # calculate shanten and store shanten = self.shanten.calculate_shanten(tiles_34, self.player.open_hand_34_tiles) results.append((shanten, tile)) # return tile to hand tiles_34[tile] += 1 (shanten, discard_34) = min(results) discard_136 = TilesConverter.find_34_tile_in_136_array(discard_34, self.player.closed_hand) if discard_136 is None: logger.debug('Greedy search or tile conversion failed') discard_136 = random.randrange(len(self.player.tiles) - 1) discard_136 = self.player.tiles[discard_136] logger.info('Shanten after discard:' + str(shanten)) return discard_136 def should_call_riichi(self): return True def should_call_win(self, tile, enemy_seat): return True def should_call_kan(self, tile, open_kan): """ When bot can call kan or chankan this method will be called :param tile: 136 tile format :param is_open_kan: boolean :return: kan type (Meld.KAN, Meld.CHANKAN) or None """ if open_kan: # don't start open hand from called kan if not self.player.is_open_hand: return None # don't call open kan if not waiting for win if not self.player.in_tempai: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # upgrade open pon to kan if possible if pon_melds: for meld in pon_melds: if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # check for improvement in shanten if new_shanten <= previous_shanten: return Meld.KAN return 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 # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables def meldDiscard(self, meld_34, discardtile): tiles_34 = TilesConverter.to_34_array(self.player.tiles + [discardtile]) closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand + [discardtile]) open_hand_34 = copy.deepcopy(self.player.open_hand_34_tiles) # remove meld from closed and and add to open hand open_hand_34.append(meld_34) for tile_34 in meld_34: closed_tiles_34[tile_34] -= 1 results = [] for tile in range(0, 34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand tiles_34[tile] -= 1 # calculate shanten and store shanten = self.shanten.calculate_shanten(tiles_34, open_hand_34) results.append((shanten, tile)) # return tile to hand tiles_34[tile] += 1 (shanten, discard_34) = min(results) discard_136 = TilesConverter.find_34_tile_in_136_array(discard_34, self.player.closed_hand) return shanten, discard_136
def __init__(self, player): super(ImplementationAI, self).__init__(player) self.shanten = Shanten() # self.agari = Agari() self.hand_divider = HandDivider()
class HandCalculator: config = None ERR_NO_WINNING_TILE = "winning_tile_not_in_hand" ERR_OPEN_HAND_RIICHI = "open_hand_riichi_not_allowed" ERR_OPEN_HAND_DABURI = "open_hand_daburi_not_allowed" ERR_IPPATSU_WITHOUT_RIICHI = "ippatsu_without_riichi_not_allowed" ERR_HAND_NOT_WINNING = "hand_not_winning" ERR_NO_YAKU = "no_yaku" ERR_CHANKAN_WITH_TSUMO = "chankan_with_tsumo_not_allowed" ERR_RINSHAN_WITHOUT_TSUMO = "rinshan_without_tsumo_not_allowed" ERR_HAITEI_WITHOUT_TSUMO = "haitei_without_tsumo_not_allowed" ERR_HOUTEI_WITH_TSUMO = "houtei_with_tsumo_not_allowed" ERR_HAITEI_WITH_RINSHAN = "haitei_with_rinshan_not_allowed" ERR_HOUTEI_WITH_CHANKAN = "houtei_with_chankan_not_allowed" ERR_TENHOU_NOT_AS_DEALER = "tenhou_not_as_dealer_not_allowed" ERR_TENHOU_WITHOUT_TSUMO = "tenhou_without_tsumo_not_allowed" ERR_TENHOU_WITH_MELD = "tenhou_with_meld_not_allowed" ERR_CHIIHOU_AS_DEALER = "chiihou_as_dealer_not_allowed" ERR_CHIIHOU_WITHOUT_TSUMO = "chiihou_without_tsumo_not_allowed" ERR_CHIIHOU_WITH_MELD = "chiihou_with_meld_not_allowed" ERR_RENHOU_AS_DEALER = "renhou_as_dealer_not_allowed" ERR_RENHOU_WITH_TSUMO = "renhou_with_tsumo_not_allowed" ERR_RENHOU_WITH_MELD = "renhou_with_meld_not_allowed" # more possible errors, like tenhou and haitei can't be together (so complicated :<) def __init__(self): self.divider = HandDivider() def estimate_hand_value( self, tiles, win_tile, melds=None, dora_indicators=None, config=None, scores_calculator_factory=ScoresCalculator, use_hand_divider_cache=False, ): """ :param tiles: array with 14 tiles in 136-tile format :param win_tile: 136 format tile that caused win (ron or tsumo) :param melds: array with Meld objects :param dora_indicators: array of tiles in 136-tile format :param config: HandConfig object :param use_hand_divider_cache: could be useful if you are calculating a lot of menchin hands :return: HandResponse object """ if not melds: melds = [] if not dora_indicators: dora_indicators = [] self.config = config or HandConfig() agari = Agari() hand_yaku = [] scores_calculator = scores_calculator_factory() tiles_34 = TilesConverter.to_34_array(tiles) fu_calculator = FuCalculator() is_aotenjou = isinstance(scores_calculator, Aotenjou) opened_melds = [x.tiles_34 for x in melds if x.opened] all_melds = [x.tiles_34 for x in melds] is_open_hand = len(opened_melds) > 0 # special situation if self.config.is_nagashi_mangan: hand_yaku.append(self.config.yaku.nagashi_mangan) fu = 30 han = self.config.yaku.nagashi_mangan.han_closed cost = scores_calculator.calculate_scores(han, fu, self.config, False) return HandResponse(cost, han, fu, hand_yaku) if win_tile not in tiles: return HandResponse(error=HandCalculator.ERR_NO_WINNING_TILE) if self.config.is_riichi and not self.config.is_daburu_riichi and is_open_hand: return HandResponse(error=HandCalculator.ERR_OPEN_HAND_RIICHI) if self.config.is_daburu_riichi and is_open_hand: return HandResponse(error=HandCalculator.ERR_OPEN_HAND_DABURI) if self.config.is_ippatsu and not self.config.is_riichi and not self.config.is_daburu_riichi: return HandResponse( error=HandCalculator.ERR_IPPATSU_WITHOUT_RIICHI) if self.config.is_chankan and self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_CHANKAN_WITH_TSUMO) if self.config.is_rinshan and not self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_RINSHAN_WITHOUT_TSUMO) if self.config.is_haitei and not self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_HAITEI_WITHOUT_TSUMO) if self.config.is_houtei and self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_HOUTEI_WITH_TSUMO) if self.config.is_haitei and self.config.is_rinshan: return HandResponse(error=HandCalculator.ERR_HAITEI_WITH_RINSHAN) if self.config.is_houtei and self.config.is_chankan: return HandResponse(error=HandCalculator.ERR_HOUTEI_WITH_CHANKAN) # raise error only when player wind is defined (and is *not* EAST) if self.config.is_tenhou and self.config.player_wind and not self.config.is_dealer: return HandResponse(error=HandCalculator.ERR_TENHOU_NOT_AS_DEALER) if self.config.is_tenhou and not self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_TENHOU_WITHOUT_TSUMO) if self.config.is_tenhou and melds: return HandResponse(error=HandCalculator.ERR_TENHOU_WITH_MELD) # raise error only when player wind is defined (and is EAST) if self.config.is_chiihou and self.config.player_wind and self.config.is_dealer: return HandResponse(error=HandCalculator.ERR_CHIIHOU_AS_DEALER) if self.config.is_chiihou and not self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_CHIIHOU_WITHOUT_TSUMO) if self.config.is_chiihou and melds: return HandResponse(error=HandCalculator.ERR_CHIIHOU_WITH_MELD) # raise error only when player wind is defined (and is EAST) if self.config.is_renhou and self.config.player_wind and self.config.is_dealer: return HandResponse(error=HandCalculator.ERR_RENHOU_AS_DEALER) if self.config.is_renhou and self.config.is_tsumo: return HandResponse(error=HandCalculator.ERR_RENHOU_WITH_TSUMO) if self.config.is_renhou and melds: return HandResponse(error=HandCalculator.ERR_RENHOU_WITH_MELD) if not agari.is_agari(tiles_34, all_melds): return HandResponse(error=HandCalculator.ERR_HAND_NOT_WINNING) if not self.config.options.has_double_yakuman: self.config.yaku.daburu_kokushi.han_closed = 13 self.config.yaku.suuankou_tanki.han_closed = 13 self.config.yaku.daburu_chuuren_poutou.han_closed = 13 self.config.yaku.daisuushi.han_closed = 13 self.config.yaku.daisuushi.han_open = 13 hand_options = self.divider.divide_hand( tiles_34, melds, use_cache=use_hand_divider_cache) calculated_hands = [] for hand in hand_options: is_chiitoitsu = self.config.yaku.chiitoitsu.is_condition_met(hand) valued_tiles = [ HAKU, HATSU, CHUN, self.config.player_wind, self.config.round_wind ] win_groups = self._find_win_groups(win_tile, hand, opened_melds) for win_group in win_groups: cost = None error = None hand_yaku = [] han = 0 fu_details, fu = fu_calculator.calculate_fu( hand, win_tile, win_group, self.config, valued_tiles, melds) is_pinfu = len( fu_details) == 1 and not is_chiitoitsu and not is_open_hand pon_sets = [x for x in hand if is_pon(x)] kan_sets = [x for x in hand if is_kan(x)] chi_sets = [x for x in hand if is_chi(x)] if self.config.is_tsumo: if not is_open_hand: hand_yaku.append(self.config.yaku.tsumo) if is_pinfu: hand_yaku.append(self.config.yaku.pinfu) # let's skip hand that looks like chitoitsu, but it contains open sets if is_chiitoitsu and is_open_hand: continue if is_chiitoitsu: hand_yaku.append(self.config.yaku.chiitoitsu) is_daisharin = self.config.yaku.daisharin.is_condition_met( hand, self.config.options.has_daisharin_other_suits) if self.config.options.has_daisharin and is_daisharin: self.config.yaku.daisharin.rename(hand) hand_yaku.append(self.config.yaku.daisharin) if self.config.options.has_daichisei and self.config.yaku.daichisei.is_condition_met( hand): hand_yaku.append(self.config.yaku.daichisei) is_tanyao = self.config.yaku.tanyao.is_condition_met(hand) if is_open_hand and not self.config.options.has_open_tanyao: is_tanyao = False if is_tanyao: hand_yaku.append(self.config.yaku.tanyao) if self.config.is_riichi and not self.config.is_daburu_riichi: if self.config.is_open_riichi: hand_yaku.append(self.config.yaku.open_riichi) else: hand_yaku.append(self.config.yaku.riichi) if self.config.is_daburu_riichi: if self.config.is_open_riichi: hand_yaku.append(self.config.yaku.daburu_open_riichi) else: hand_yaku.append(self.config.yaku.daburu_riichi) if (not self.config.is_tsumo and self.config.options.has_sashikomi_yakuman and ((self.config.yaku.daburu_open_riichi in hand_yaku) or (self.config.yaku.open_riichi in hand_yaku))): hand_yaku.append(self.config.yaku.sashikomi) if self.config.is_ippatsu: hand_yaku.append(self.config.yaku.ippatsu) if self.config.is_rinshan: hand_yaku.append(self.config.yaku.rinshan) if self.config.is_chankan: hand_yaku.append(self.config.yaku.chankan) if self.config.is_haitei: hand_yaku.append(self.config.yaku.haitei) if self.config.is_houtei: hand_yaku.append(self.config.yaku.houtei) if self.config.is_renhou: if self.config.options.renhou_as_yakuman: hand_yaku.append(self.config.yaku.renhou_yakuman) else: hand_yaku.append(self.config.yaku.renhou) if self.config.is_tenhou: hand_yaku.append(self.config.yaku.tenhou) if self.config.is_chiihou: hand_yaku.append(self.config.yaku.chiihou) if self.config.yaku.honitsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.honitsu) if self.config.yaku.chinitsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.chinitsu) if self.config.yaku.tsuisou.is_condition_met(hand): hand_yaku.append(self.config.yaku.tsuisou) if self.config.yaku.honroto.is_condition_met(hand): hand_yaku.append(self.config.yaku.honroto) if self.config.yaku.chinroto.is_condition_met(hand): hand_yaku.append(self.config.yaku.chinroto) if self.config.yaku.ryuisou.is_condition_met(hand): hand_yaku.append(self.config.yaku.ryuisou) if self.config.paarenchan > 0 and not self.config.options.paarenchan_needs_yaku: # if no yaku is even needed to win on paarenchan and it is paarenchan condition, just add paarenchan self.config.yaku.paarenchan.set_paarenchan_count( self.config.paarenchan) hand_yaku.append(self.config.yaku.paarenchan) # small optimization, try to detect yaku with chi required sets only if we have chi sets in hand if len(chi_sets): if self.config.yaku.chantai.is_condition_met(hand): hand_yaku.append(self.config.yaku.chantai) if self.config.yaku.junchan.is_condition_met(hand): hand_yaku.append(self.config.yaku.junchan) if self.config.yaku.ittsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.ittsu) if not is_open_hand: if self.config.yaku.ryanpeiko.is_condition_met(hand): hand_yaku.append(self.config.yaku.ryanpeiko) elif self.config.yaku.iipeiko.is_condition_met(hand): hand_yaku.append(self.config.yaku.iipeiko) if self.config.yaku.sanshoku.is_condition_met(hand): hand_yaku.append(self.config.yaku.sanshoku) # small optimization, try to detect yaku with pon required sets only if we have pon sets in hand if len(pon_sets) or len(kan_sets): if self.config.yaku.toitoi.is_condition_met(hand): hand_yaku.append(self.config.yaku.toitoi) if self.config.yaku.sanankou.is_condition_met( hand, win_tile, melds, self.config.is_tsumo): hand_yaku.append(self.config.yaku.sanankou) if self.config.yaku.sanshoku_douko.is_condition_met(hand): hand_yaku.append(self.config.yaku.sanshoku_douko) if self.config.yaku.shosangen.is_condition_met(hand): hand_yaku.append(self.config.yaku.shosangen) if self.config.yaku.haku.is_condition_met(hand): hand_yaku.append(self.config.yaku.haku) if self.config.yaku.hatsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.hatsu) if self.config.yaku.chun.is_condition_met(hand): hand_yaku.append(self.config.yaku.chun) if self.config.yaku.east.is_condition_met( hand, self.config.player_wind, self.config.round_wind): if self.config.player_wind == EAST: hand_yaku.append(self.config.yaku.yakuhai_place) if self.config.round_wind == EAST: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.south.is_condition_met( hand, self.config.player_wind, self.config.round_wind): if self.config.player_wind == SOUTH: hand_yaku.append(self.config.yaku.yakuhai_place) if self.config.round_wind == SOUTH: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.west.is_condition_met( hand, self.config.player_wind, self.config.round_wind): if self.config.player_wind == WEST: hand_yaku.append(self.config.yaku.yakuhai_place) if self.config.round_wind == WEST: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.north.is_condition_met( hand, self.config.player_wind, self.config.round_wind): if self.config.player_wind == NORTH: hand_yaku.append(self.config.yaku.yakuhai_place) if self.config.round_wind == NORTH: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.daisangen.is_condition_met(hand): hand_yaku.append(self.config.yaku.daisangen) if self.config.yaku.shosuushi.is_condition_met(hand): hand_yaku.append(self.config.yaku.shosuushi) if self.config.yaku.daisuushi.is_condition_met(hand): hand_yaku.append(self.config.yaku.daisuushi) # closed kan can't be used in chuuren_poutou if not len( melds ) and self.config.yaku.chuuren_poutou.is_condition_met( hand): if tiles_34[win_tile // 4] == 2 or tiles_34[win_tile // 4] == 4: hand_yaku.append( self.config.yaku.daburu_chuuren_poutou) else: hand_yaku.append(self.config.yaku.chuuren_poutou) if not is_open_hand and self.config.yaku.suuankou.is_condition_met( hand, win_tile, self.config.is_tsumo): if tiles_34[win_tile // 4] == 2: hand_yaku.append(self.config.yaku.suuankou_tanki) else: hand_yaku.append(self.config.yaku.suuankou) if self.config.yaku.sankantsu.is_condition_met( hand, melds): hand_yaku.append(self.config.yaku.sankantsu) if self.config.yaku.suukantsu.is_condition_met( hand, melds): hand_yaku.append(self.config.yaku.suukantsu) if self.config.paarenchan > 0 and self.config.options.paarenchan_needs_yaku and len( hand_yaku) > 0: # we waited until here to add paarenchan yakuman only if there is any other yaku self.config.yaku.paarenchan.set_paarenchan_count( self.config.paarenchan) hand_yaku.append(self.config.yaku.paarenchan) # yakuman is not connected with other yaku yakuman_list = [x for x in hand_yaku if x.is_yakuman] if yakuman_list: if not is_aotenjou: hand_yaku = yakuman_list else: scores_calculator.aotenjou_filter_yaku( hand_yaku, self.config) yakuman_list = [] # calculate han for item in hand_yaku: if is_open_hand and item.han_open: han += item.han_open else: han += item.han_closed if han == 0: error = HandCalculator.ERR_NO_YAKU cost = None # we don't need to add dora to yakuman if not yakuman_list: tiles_for_dora = tiles[:] count_of_dora = 0 count_of_aka_dora = 0 for tile in tiles_for_dora: count_of_dora += plus_dora(tile, dora_indicators) for tile in tiles_for_dora: if is_aka_dora(tile, self.config.options.has_aka_dora): count_of_aka_dora += 1 if count_of_dora: self.config.yaku.dora.han_open = count_of_dora self.config.yaku.dora.han_closed = count_of_dora hand_yaku.append(self.config.yaku.dora) han += count_of_dora if count_of_aka_dora: self.config.yaku.aka_dora.han_open = count_of_aka_dora self.config.yaku.aka_dora.han_closed = count_of_aka_dora hand_yaku.append(self.config.yaku.aka_dora) han += count_of_aka_dora if not is_aotenjou and ( self.config.options.limit_to_sextuple_yakuman and han > 78): han = 78 if fu == 0 and is_aotenjou: fu = 40 if not error: cost = scores_calculator.calculate_scores( han, fu, self.config, len(yakuman_list) > 0) calculated_hand = { "cost": cost, "error": error, "hand_yaku": hand_yaku, "han": han, "fu": fu, "fu_details": fu_details, } calculated_hands.append(calculated_hand) # exception hand if not is_open_hand and self.config.yaku.kokushi.is_condition_met( None, tiles_34): if tiles_34[win_tile // 4] == 2: hand_yaku.append(self.config.yaku.daburu_kokushi) else: hand_yaku.append(self.config.yaku.kokushi) if not self.config.is_tsumo and self.config.options.has_sashikomi_yakuman: if self.config.is_riichi and not self.config.is_daburu_riichi: if self.config.is_open_riichi: hand_yaku.append(self.config.yaku.sashikomi) if self.config.is_daburu_riichi: if self.config.is_open_riichi: hand_yaku.append(self.config.yaku.sashikomi) if self.config.is_renhou and self.config.options.renhou_as_yakuman: hand_yaku.append(self.config.yaku.renhou_yakuman) if self.config.is_tenhou: hand_yaku.append(self.config.yaku.tenhou) if self.config.is_chiihou: hand_yaku.append(self.config.yaku.chiihou) if self.config.paarenchan > 0: self.config.yaku.paarenchan.set_paarenchan_count( self.config.paarenchan) hand_yaku.append(self.config.yaku.paarenchan) # calculate han han = 0 for item in hand_yaku: if is_open_hand and item.han_open: han += item.han_open else: han += item.han_closed fu = 0 if is_aotenjou: if self.config.is_tsumo: fu = 30 else: fu = 40 tiles_for_dora = tiles[:] count_of_dora = 0 count_of_aka_dora = 0 for tile in tiles_for_dora: count_of_dora += plus_dora(tile, dora_indicators) for tile in tiles_for_dora: if is_aka_dora(tile, self.config.options.has_aka_dora): count_of_aka_dora += 1 if count_of_dora: self.config.yaku.dora.han_open = count_of_dora self.config.yaku.dora.han_closed = count_of_dora hand_yaku.append(self.config.yaku.dora) han += count_of_dora if count_of_aka_dora: self.config.yaku.aka_dora.han_open = count_of_aka_dora self.config.yaku.aka_dora.han_closed = count_of_aka_dora hand_yaku.append(self.config.yaku.aka_dora) han += count_of_aka_dora cost = scores_calculator.calculate_scores(han, fu, self.config, len(hand_yaku) > 0) calculated_hands.append({ "cost": cost, "error": None, "hand_yaku": hand_yaku, "han": han, "fu": fu, "fu_details": [] }) # let's use cost for most expensive hand calculated_hands = sorted(calculated_hands, key=lambda x: (x["han"], x["fu"]), reverse=True) calculated_hand = calculated_hands[0] cost = calculated_hand["cost"] error = calculated_hand["error"] hand_yaku = calculated_hand["hand_yaku"] han = calculated_hand["han"] fu = calculated_hand["fu"] fu_details = calculated_hand["fu_details"] return HandResponse(cost, han, fu, hand_yaku, error, fu_details, is_open_hand) def _find_win_groups(self, win_tile, hand, opened_melds): win_tile_34 = (win_tile or 0) // 4 # to detect win groups # we had to use only closed sets closed_set_items = [] for x in hand: if x not in opened_melds: closed_set_items.append(x) else: opened_melds.remove(x) # for forms like 45666 and ron on 6 # we can assume that ron was on 456 form and on 66 form # and depends on form we will have different hand cost # so, we had to check all possible win groups win_groups = [x for x in closed_set_items if win_tile_34 in x] unique_win_groups = [ list(x) for x in set(tuple(x) for x in win_groups) ] return unique_win_groups
def __init__(self): self.divider = HandDivider()
def estimate_hand_value(self, tiles, win_tile, melds=None, dora_indicators=None, config=None): """ :param tiles: array with 14 tiles in 136-tile format :param win_tile: 136 format tile that caused win (ron or tsumo) :param melds: array with Meld objects :param dora_indicators: array of tiles in 136-tile format :param config: HandConfig object :return: HandResponse object """ if not melds: melds = [] if not dora_indicators: dora_indicators = [] self.config = config or HandConfig() agari = Agari() hand_yaku = [] scores_calculator = ScoresCalculator() tiles_34 = TilesConverter.to_34_array(tiles) divider = HandDivider() fu_calculator = FuCalculator() opened_melds = [x.tiles_34 for x in melds if x.opened] all_melds = [x.tiles_34 for x in melds] is_open_hand = len(opened_melds) > 0 # special situation if self.config.is_nagashi_mangan: hand_yaku.append(self.config.yaku.nagashi_mangan) fu = 30 han = self.config.yaku.nagashi_mangan.han_closed cost = scores_calculator.calculate_scores(han, fu, self.config, False) return HandResponse(cost, han, fu, hand_yaku) if win_tile not in tiles: return HandResponse(error="Win tile not in the hand") if self.config.is_riichi and is_open_hand: return HandResponse( error="Riichi can't be declared with open hand") if self.config.is_ippatsu and is_open_hand: return HandResponse( error="Ippatsu can't be declared with open hand") if self.config.is_ippatsu and not self.config.is_riichi and not self.config.is_daburu_riichi: return HandResponse( error="Ippatsu can't be declared without riichi") if not agari.is_agari(tiles_34, all_melds): return HandResponse(error='Hand is not winning') if not self.config.options.has_double_yakuman: self.config.yaku.daburu_kokushi.han_closed = 13 self.config.yaku.suuankou_tanki.han_closed = 13 self.config.yaku.daburu_chuuren_poutou.han_closed = 13 self.config.yaku.daisuushi.han_closed = 13 self.config.yaku.daisuushi.han_open = 13 hand_options = divider.divide_hand(tiles_34, melds) calculated_hands = [] for hand in hand_options: is_chiitoitsu = self.config.yaku.chiitoitsu.is_condition_met(hand) valued_tiles = [ HAKU, HATSU, CHUN, self.config.player_wind, self.config.round_wind ] win_groups = self._find_win_groups(win_tile, hand, opened_melds) for win_group in win_groups: cost = None error = None hand_yaku = [] han = 0 fu_details, fu = fu_calculator.calculate_fu( hand, win_tile, win_group, self.config, valued_tiles, melds) is_pinfu = len( fu_details) == 1 and not is_chiitoitsu and not is_open_hand pon_sets = [x for x in hand if is_pon(x)] chi_sets = [x for x in hand if is_chi(x)] if self.config.is_tsumo: if not is_open_hand: hand_yaku.append(self.config.yaku.tsumo) if is_pinfu: hand_yaku.append(self.config.yaku.pinfu) # let's skip hand that looks like chitoitsu, but it contains open sets if is_chiitoitsu and is_open_hand: continue if is_chiitoitsu: hand_yaku.append(self.config.yaku.chiitoitsu) is_daisharin = self.config.yaku.daisharin.is_condition_met( hand, self.config.options.has_daisharin_other_suits) if self.config.options.has_daisharin and is_daisharin: self.config.yaku.daisharin.rename(hand) hand_yaku.append(self.config.yaku.daisharin) is_tanyao = self.config.yaku.tanyao.is_condition_met(hand) if is_open_hand and not self.config.options.has_open_tanyao: is_tanyao = False if is_tanyao: hand_yaku.append(self.config.yaku.tanyao) if self.config.is_riichi and not self.config.is_daburu_riichi: hand_yaku.append(self.config.yaku.riichi) if self.config.is_daburu_riichi: hand_yaku.append(self.config.yaku.daburu_riichi) if self.config.is_ippatsu: hand_yaku.append(self.config.yaku.ippatsu) if self.config.is_rinshan: hand_yaku.append(self.config.yaku.rinshan) if self.config.is_chankan: hand_yaku.append(self.config.yaku.chankan) if self.config.is_haitei: hand_yaku.append(self.config.yaku.haitei) if self.config.is_houtei: hand_yaku.append(self.config.yaku.houtei) if self.config.is_renhou: if self.config.options.renhou_as_yakuman: hand_yaku.append(self.config.yaku.renhou_yakuman) else: hand_yaku.append(self.config.yaku.renhou) if self.config.is_tenhou: hand_yaku.append(self.config.yaku.tenhou) if self.config.is_chiihou: hand_yaku.append(self.config.yaku.chiihou) if self.config.yaku.honitsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.honitsu) if self.config.yaku.chinitsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.chinitsu) if self.config.yaku.tsuisou.is_condition_met(hand): hand_yaku.append(self.config.yaku.tsuisou) if self.config.yaku.honroto.is_condition_met(hand): hand_yaku.append(self.config.yaku.honroto) if self.config.yaku.chinroto.is_condition_met(hand): hand_yaku.append(self.config.yaku.chinroto) # small optimization, try to detect yaku with chi required sets only if we have chi sets in hand if len(chi_sets): if self.config.yaku.chanta.is_condition_met(hand): hand_yaku.append(self.config.yaku.chanta) if self.config.yaku.junchan.is_condition_met(hand): hand_yaku.append(self.config.yaku.junchan) if self.config.yaku.ittsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.ittsu) if not is_open_hand: if self.config.yaku.ryanpeiko.is_condition_met(hand): hand_yaku.append(self.config.yaku.ryanpeiko) elif self.config.yaku.iipeiko.is_condition_met(hand): hand_yaku.append(self.config.yaku.iipeiko) if self.config.yaku.sanshoku.is_condition_met(hand): hand_yaku.append(self.config.yaku.sanshoku) # small optimization, try to detect yaku with pon required sets only if we have pon sets in hand if len(pon_sets): if self.config.yaku.toitoi.is_condition_met(hand): hand_yaku.append(self.config.yaku.toitoi) if self.config.yaku.sanankou.is_condition_met( hand, win_tile, melds, self.config.is_tsumo): hand_yaku.append(self.config.yaku.sanankou) if self.config.yaku.sanshoku_douko.is_condition_met(hand): hand_yaku.append(self.config.yaku.sanshoku_douko) if self.config.yaku.shosangen.is_condition_met(hand): hand_yaku.append(self.config.yaku.shosangen) if self.config.yaku.haku.is_condition_met(hand): hand_yaku.append(self.config.yaku.haku) if self.config.yaku.hatsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.hatsu) if self.config.yaku.chun.is_condition_met(hand): hand_yaku.append(self.config.yaku.chun) if self.config.yaku.east.is_condition_met( hand, self.config.player_wind, self.config.round_wind): if self.config.player_wind == EAST: hand_yaku.append(self.config.yaku.yakuhai_place) if self.config.round_wind == EAST: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.south.is_condition_met( hand, self.config.player_wind, self.config.round_wind): if self.config.player_wind == SOUTH: hand_yaku.append(self.config.yaku.yakuhai_place) if self.config.round_wind == SOUTH: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.west.is_condition_met( hand, self.config.player_wind, self.config.round_wind): if self.config.player_wind == WEST: hand_yaku.append(self.config.yaku.yakuhai_place) if self.config.round_wind == WEST: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.north.is_condition_met( hand, self.config.player_wind, self.config.round_wind): if self.config.player_wind == NORTH: hand_yaku.append(self.config.yaku.yakuhai_place) if self.config.round_wind == NORTH: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.daisangen.is_condition_met(hand): hand_yaku.append(self.config.yaku.daisangen) if self.config.yaku.shosuushi.is_condition_met(hand): hand_yaku.append(self.config.yaku.shosuushi) if self.config.yaku.daisuushi.is_condition_met(hand): hand_yaku.append(self.config.yaku.daisuushi) if self.config.yaku.ryuisou.is_condition_met(hand): hand_yaku.append(self.config.yaku.ryuisou) # closed kan can't be used in chuuren_poutou if not len( melds ) and self.config.yaku.chuuren_poutou.is_condition_met( hand): if tiles_34[win_tile // 4] == 2 or tiles_34[win_tile // 4] == 4: hand_yaku.append( self.config.yaku.daburu_chuuren_poutou) else: hand_yaku.append(self.config.yaku.chuuren_poutou) if not is_open_hand and self.config.yaku.suuankou.is_condition_met( hand, win_tile, self.config.is_tsumo): if tiles_34[win_tile // 4] == 2: hand_yaku.append(self.config.yaku.suuankou_tanki) else: hand_yaku.append(self.config.yaku.suuankou) if self.config.yaku.sankantsu.is_condition_met( hand, melds): hand_yaku.append(self.config.yaku.sankantsu) if self.config.yaku.suukantsu.is_condition_met( hand, melds): hand_yaku.append(self.config.yaku.suukantsu) # yakuman is not connected with other yaku yakuman_list = [x for x in hand_yaku if x.is_yakuman] if yakuman_list: hand_yaku = yakuman_list # calculate han for item in hand_yaku: if is_open_hand and item.han_open: han += item.han_open else: han += item.han_closed if han == 0: error = 'There are no yaku in the hand' cost = None # we don't need to add dora to yakuman if not yakuman_list: tiles_for_dora = tiles[:] # we had to search for dora in kan fourth tiles as well for meld in melds: if meld.type == Meld.KAN or meld.type == Meld.CHANKAN: tiles_for_dora.append(meld.tiles[3]) count_of_dora = 0 count_of_aka_dora = 0 for tile in tiles_for_dora: count_of_dora += plus_dora(tile, dora_indicators) for tile in tiles_for_dora: if is_aka_dora(tile, self.config.options.has_aka_dora): count_of_aka_dora += 1 if count_of_dora: self.config.yaku.dora.han_open = count_of_dora self.config.yaku.dora.han_closed = count_of_dora hand_yaku.append(self.config.yaku.dora) han += count_of_dora if count_of_aka_dora: self.config.yaku.aka_dora.han_open = count_of_aka_dora self.config.yaku.aka_dora.han_closed = count_of_aka_dora hand_yaku.append(self.config.yaku.aka_dora) han += count_of_aka_dora if not error: cost = scores_calculator.calculate_scores( han, fu, self.config, len(yakuman_list) > 0) calculated_hand = { 'cost': cost, 'error': error, 'hand_yaku': hand_yaku, 'han': han, 'fu': fu, 'fu_details': fu_details } calculated_hands.append(calculated_hand) # exception hand if not is_open_hand and self.config.yaku.kokushi.is_condition_met( None, tiles_34): if tiles_34[win_tile // 4] == 2: hand_yaku.append(self.config.yaku.daburu_kokushi) else: hand_yaku.append(self.config.yaku.kokushi) if self.config.is_renhou and self.config.options.renhou_as_yakuman: hand_yaku.append(self.config.yaku.renhou_yakuman) if self.config.is_tenhou: hand_yaku.append(self.config.yaku.tenhou) if self.config.is_chiihou: hand_yaku.append(self.config.yaku.chiihou) # calculate han han = 0 for item in hand_yaku: if is_open_hand and item.han_open: han += item.han_open else: han += item.han_closed fu = 0 cost = scores_calculator.calculate_scores(han, fu, self.config, len(hand_yaku) > 0) calculated_hands.append({ 'cost': cost, 'error': None, 'hand_yaku': hand_yaku, 'han': han, 'fu': fu, 'fu_details': [] }) # let's use cost for most expensive hand calculated_hands = sorted(calculated_hands, key=lambda x: (x['han'], x['fu']), reverse=True) calculated_hand = calculated_hands[0] cost = calculated_hand['cost'] error = calculated_hand['error'] hand_yaku = calculated_hand['hand_yaku'] han = calculated_hand['han'] fu = calculated_hand['fu'] fu_details = calculated_hand['fu_details'] return HandResponse(cost, han, fu, hand_yaku, error, fu_details)