def _initialize_honitsu_dora_count(self, tiles_136, suit): tiles_34 = TilesConverter.to_34_array(tiles_136) dora_count_man_not_isolated = 0 dora_count_pin_not_isolated = 0 dora_count_sou_not_isolated = 0 for tile_136 in tiles_136: tile_34 = tile_136 // 4 dora_count = plus_dora(tile_136, self.player.table.dora_indicators) if is_aka_dora(tile_136, self.player.table.has_aka_dora): dora_count += 1 if is_man(tile_34): if not is_tile_strictly_isolated(tiles_34, tile_34): dora_count_man_not_isolated += dora_count if is_pin(tile_34): if not is_tile_strictly_isolated(tiles_34, tile_34): dora_count_pin_not_isolated += dora_count if is_sou(tile_34): if not is_tile_strictly_isolated(tiles_34, tile_34): dora_count_sou_not_isolated += dora_count if suit['name'] == 'pin': self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated elif suit['name'] == 'sou': self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated elif suit['name'] == 'man': self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated
def calculate_dora_count(self, tiles_136): self.dora_count_central = 0 self.dora_count_not_central = 0 self.aka_dora_count = 0 for tile_136 in tiles_136: tile_34 = tile_136 // 4 dora_count = plus_dora(tile_136, self.player.table.dora_indicators) if is_aka_dora(tile_136, self.player.table.has_aka_dora): self.aka_dora_count += 1 if not dora_count: continue if is_honor(tile_34): self.dora_count_not_central += dora_count self.dora_count_honor += dora_count elif is_terminal(tile_34): self.dora_count_not_central += dora_count else: self.dora_count_central += dora_count self.dora_count_central += self.aka_dora_count self.dora_count_total = self.dora_count_central + self.dora_count_not_central
def calculate_dora_count(self, tiles_136): self.dora_count_central = 0 self.dora_count_not_central = 0 self.aka_dora_count = 0 for tile_136 in tiles_136: tile_34 = tile_136 // 4 dora_count = plus_dora( tile_136, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora ) if not dora_count: continue if is_honor(tile_34): self.dora_count_not_central += dora_count self.dora_count_honor += dora_count elif is_terminal(tile_34): self.dora_count_not_central += dora_count else: self.dora_count_central += dora_count self.dora_count_central += self.aka_dora_count self.dora_count_total = self.dora_count_central + self.dora_count_not_central
def get_bonus_danger(self, tile_136, number_of_revealed_tiles): bonus_danger = [] tile_34 = tile_136 // 4 number_of_yakuhai = self.enemy.valued_honors.count(tile_34) # shonpai tiles if number_of_revealed_tiles == 1: # aka doras don't get additional danger against toitoi, they just get their regular one dora_count = plus_dora(tile_136, self.enemy.table.dora_indicators) if dora_count > 0: danger = copy(TileDanger.TOITOI_SHONPAI_DORA_BONUS_DANGER) danger["value"] = dora_count * danger["value"] danger["dora_count"] = dora_count bonus_danger.append(danger) if number_of_yakuhai > 0: bonus_danger.append(TileDanger.TOITOI_SHONPAI_YAKUHAI_BONUS_DANGER) else: bonus_danger.append(TileDanger.TOITOI_SHONPAI_NON_YAKUHAI_BONUS_DANGER) elif number_of_revealed_tiles == 2: if number_of_yakuhai > 0: bonus_danger.append(TileDanger.TOITOI_SECOND_YAKUHAI_HONOR_BONUS_DANGER) elif number_of_revealed_tiles == 3: # FIXME: we should add negative bonus danger exclusively against toitoi for such tiles # except for doras and honors maybe pass return bonus_danger
def is_threatening(self): """ Should we fold against this player or not :return: boolean """ if self.player.in_riichi: return True discards = self.player.discards discards_34 = TilesConverter.to_34_array([x.value for x in discards]) is_honitsu_open_sets, open_hand_suit = False, None is_honitsu_discards, discard_suit = self._is_honitsu_discards( discards_34) meld_tiles = self.player.meld_tiles meld_tiles_34 = TilesConverter.to_34_array(meld_tiles) if meld_tiles: dora_count = sum( [plus_dora(x, self.table.dora_indicators) for x in meld_tiles]) # aka dora dora_count += sum([ 1 for x in meld_tiles if is_aka_dora(x, self.table.has_open_tanyao) ]) # enemy has a lot of dora tiles in his opened sets # so better to fold against him if dora_count >= 3: return True # check that user has a discard and melds that looks like honitsu is_honitsu_open_sets, open_hand_suit = self._is_honitsu_open_sets( meld_tiles_34) if is_honitsu_open_sets: # for 2 opened melds we had to check discard, to be sure if len( self.player.melds ) <= 2 and is_honitsu_discards and discard_suit == open_hand_suit: self.chosen_suit = open_hand_suit return True # for 3+ opened melds there is no sense to check discard if len(self.player.melds) >= 3: self.chosen_suit = open_hand_suit return True return False
def calculate_value(self, shanten=None): # base is 100 for ability to mark tiles as not needed (like set value to 50) value = 100 honored_value = 20 # we don't need to keep honor tiles in almost completed hand if shanten and shanten <= 2: honored_value = 0 if is_honor(self.tile_to_discard): if self.tile_to_discard in self.player.valued_honors: count_of_winds = [ x for x in self.player.valued_honors if x == self.tile_to_discard ] # for west-west, east-east we had to double tile value value += honored_value * len(count_of_winds) else: # suits suit_tile_grades = [10, 20, 30, 40, 50, 40, 30, 20, 10] simplified_tile = simplify(self.tile_to_discard) value += suit_tile_grades[simplified_tile] count_of_dora = plus_dora(self.tile_to_discard * 4, self.player.table.dora_indicators) if is_aka_dora(self.tile_to_discard * 4, self.player.table.has_open_tanyao): count_of_dora += 1 value += 50 * count_of_dora if is_honor(self.tile_to_discard): # depends on how much honor tiles were discarded # we will decrease tile value discard_percentage = [100, 75, 20, 0, 0] discarded_tiles = self.player.table.revealed_tiles[ self.tile_to_discard] value = (value * discard_percentage[discarded_tiles]) / 100 # three honor tiles were discarded, # so we don't need this tile anymore if value == 0: self.had_to_be_discarded = True self.valuation = value
def _initialize_honitsu_dora_count(self, tiles_136, suit): tiles_34 = TilesConverter.to_34_array(tiles_136) dora_count_man = 0 dora_count_pin = 0 dora_count_sou = 0 dora_count_man_not_isolated = 0 dora_count_pin_not_isolated = 0 dora_count_sou_not_isolated = 0 for tile_136 in tiles_136: tile_34 = tile_136 // 4 dora_count = plus_dora( tile_136, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora ) if is_man(tile_34): dora_count_man += dora_count if not is_tile_strictly_isolated(tiles_34, tile_34): dora_count_man_not_isolated += dora_count if is_pin(tile_34): dora_count_pin += dora_count if not is_tile_strictly_isolated(tiles_34, tile_34): dora_count_pin_not_isolated += dora_count if is_sou(tile_34): dora_count_sou += dora_count if not is_tile_strictly_isolated(tiles_34, tile_34): dora_count_sou_not_isolated += dora_count if suit["name"] == "pin": self.dora_count_our_suit = dora_count_pin self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated elif suit["name"] == "sou": self.dora_count_our_suit = dora_count_sou self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated elif suit["name"] == "man": self.dora_count_our_suit = dora_count_man self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated
def _get_dora_scale_bonus(self, tile_136): tile_34 = tile_136 // 4 scale_bonus = 0 dora_count = plus_dora(tile_136, self.table.dora_indicators, add_aka_dora=self.table.has_aka_dora) if is_honor(tile_34): closed_hand_34 = TilesConverter.to_34_array( self.main_player.closed_hand) revealed_tiles = self.main_player.number_of_revealed_tiles( tile_34, closed_hand_34) if revealed_tiles < 2: scale_bonus += dora_count * 3 else: scale_bonus += dora_count * 2 else: scale_bonus += dora_count return scale_bonus
def is_threatening(self): """ Should we fold against this player or not :return: boolean """ if self.player.in_riichi: return True discards = self.player.discards discards_34 = TilesConverter.to_34_array([x.value for x in discards]) is_honitsu_open_sets, open_hand_suit = False, None is_honitsu_discards, discard_suit = self._is_honitsu_discards(discards_34) meld_tiles = self.player.meld_tiles meld_tiles_34 = TilesConverter.to_34_array(meld_tiles) if meld_tiles: dora_count = sum([plus_dora(x, self.table.dora_indicators) for x in meld_tiles]) # aka dora dora_count += sum([1 for x in meld_tiles if is_aka_dora(x, self.table.has_open_tanyao)]) # enemy has a lot of dora tiles in his opened sets # so better to fold against him if dora_count >= 3: return True # check that user has a discard and melds that looks like honitsu is_honitsu_open_sets, open_hand_suit = self._is_honitsu_open_sets(meld_tiles_34) if is_honitsu_open_sets: # for 2 opened melds we had to check discard, to be sure if len(self.player.melds) <= 2 and is_honitsu_discards and discard_suit == open_hand_suit: self.chosen_suit = open_hand_suit return True # for 3+ opened melds there is no sense to check discard if len(self.player.melds) >= 3: self.chosen_suit = open_hand_suit return True return False
def _simplified_danger_valuation(self, discard_option): tile_34 = discard_option.tile_to_discard_34 tile_136 = discard_option.tile_to_discard_136 number_of_revealed_tiles = self.player.number_of_revealed_tiles( tile_34, TilesConverter.to_34_array(self.player.closed_hand)) if is_honor(tile_34): if not self.player.table.is_common_yakuhai(tile_34): if number_of_revealed_tiles == 4: simple_danger = 0 elif number_of_revealed_tiles == 3: simple_danger = 10 elif number_of_revealed_tiles == 2: simple_danger = 20 else: simple_danger = 30 else: if number_of_revealed_tiles == 4: simple_danger = 0 elif number_of_revealed_tiles == 3: simple_danger = 11 elif number_of_revealed_tiles == 2: simple_danger = 21 else: simple_danger = 32 elif is_terminal(tile_34): simple_danger = 100 elif simplify(tile_34) < 2 or simplify(tile_34) > 6: # 2, 3 or 7, 8 simple_danger = 200 else: # 4, 5, 6 simple_danger = 300 if simple_danger != 0: simple_danger += plus_dora( tile_136, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora) return simple_danger
def _suji_tiles(self, suji): suji_temp = suji % 9 base = suji - suji_temp - 1 first_danger = 20 second_danger = 30 third_danger = 40 result = [] if suji_temp == self.FIRST_SUJI: result = [ DefenceTile(base + 1, first_danger), DefenceTile(base + 4, second_danger), DefenceTile(base + 7, third_danger) ] if suji_temp == self.SECOND_SUJI: result = [ DefenceTile(base + 2, second_danger), DefenceTile(base + 5, second_danger), DefenceTile(base + 8, second_danger) ] if suji_temp == self.THIRD_SUJI: result = [ DefenceTile(base + 3, third_danger), DefenceTile(base + 6, second_danger), DefenceTile(base + 9, first_danger) ] # mark dora tiles as dangerous tiles to discard for tile in result: is_dora = plus_dora(tile.value * 4, self.table.dora_indicators) \ or is_aka_dora(tile.value * 4, self.table.has_open_tanyao) if is_dora: tile.danger += 100 return result
def should_activate_strategy(self, tiles_136): """ We can go for honitsu strategy if we have prevalence of one suit and honor tiles """ result = super(HonitsuStrategy, self).should_activate_strategy(tiles_136) if not result: return False tiles_34 = TilesConverter.to_34_array(tiles_136) suits = count_tiles_by_suits(tiles_34) suits = [x for x in suits if x['name'] != 'honor'] suits = sorted(suits, key=lambda x: x['count'], reverse=True) suit = suits[0] count_of_shuntsu_other_suits = 0 count_of_koutsu_other_suits = 0 count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[1]['function']) count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[2]['function']) count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[1]['function']) count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[2]['function']) self._calculate_not_suitable_tiles_cnt(tiles_34, suit['function']) self._initialize_honitsu_dora_count(tiles_136, suit) # let's not go for honitsu if we have 5 or more non-isolated # tiles in other suits if self.tiles_count_other_suits >= 5: return False # let's not go for honitsu if we have 2 or more non-isolated doras # in other suits if self.dora_count_other_suits_not_isolated >= 2: return False # if we have a pon of valued doras, let's not go for honitsu # we have a mangan anyway, let's go for fastest hand valued_pons = [x for x in self.player.valued_honors if tiles_34[x] >= 3] for pon in valued_pons: dora_count = plus_dora(pon * 4, self.player.table.dora_indicators) if dora_count > 0: return False valued_pairs = len([x for x in self.player.valued_honors if tiles_34[x] == 2]) honor_pairs_or_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2]) honor_doras_pairs_or_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2 and plus_dora(x * 4, self.player.table.dora_indicators)]) unvalued_singles = len([x for x in range(0, 34) if is_honor(x) and x not in self.player.valued_honors and tiles_34[x] == 1]) # if we have some decent amount of not isolated tiles in other suits # we may not rush for honitsu considering other conditions if self.tiles_count_other_suits_not_isolated >= 3: # if we don't have pair or pon of honored doras if honor_doras_pairs_or_pons == 0: # we need to either have a valued pair or have at least two honor # pairs to consider honitsu if valued_pairs == 0 and honor_pairs_or_pons < 2: return False # doesn't matter valued or not, if we have just one honor pair # and have some single unvalued tiles, let's throw them away # first if honor_pairs_or_pons == 1 and unvalued_singles >= 2: return False # 3 non-isolated unsuitable tiles, 1-shanen and already 8th turn # let's not consider honitsu here if self.player.ai.shanten == 1 and self.player.round_step > 8: return False else: # we have a pon of unvalued honor doras, but it looks like # it's faster to build our hand without honitsu if self.player.ai.shanten == 1: return False # if we have a complete set in other suits, we can only throw it away if it's early in the game if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1: # too late to throw away chi after 8 step if self.player.round_step > 8: return False # already 1 shanten, no need to throw away complete set if self.player.ai.shanten == 1: return False # dora is not isolated and we have a complete set, let's not go for honitsu if self.dora_count_other_suits_not_isolated >= 1: return False self.chosen_suit = suit['function'] return True
def should_go_to_defence_mode(self, discard_candidate=None): """ The method is decides should bot go to the defence mode or not. For now only full defence is possible :return: true|false """ # we drew a tile, so we have 14 tiles in our hand if discard_candidate: shanten = discard_candidate.shanten waiting = discard_candidate.waiting wanted_tiles_count = discard_candidate.tiles_count # we have 13 tiles in hand (this is not our turn) else: shanten = self.player.ai.previous_shanten waiting = self.player.ai.waiting wanted_tiles_count = self.player.ai.wanted_tiles_count # if we are the top, it's better to defence # if self.player == self.table.get_players_sorted_by_scores()[0] and self.player.scores > 30000: # logger.info("Player is at the 1st position, better to fold.") # return True # if we are in riichi or meld too much, we can't defence if self.player.in_riichi or self.player.ai.pushing or len( self.player.melds) >= 3: logger.info("In reach or pushing state, cannot defence.") return False # if we are the at the 4st position, it's better to push if self.player == self.table.get_players_sorted_by_scores( )[-1] and self.table.round_number >= 5: logger.info("Player is at the 4st position, better to push.") self.player.ai.pushing = True self.player.set_state("PUSHING") return False threatening_players = self._get_threatening_players() self.threatening_players = threatening_players # assign this for further calculation in other methods # no one is threatening, so we can build our hand if len(threatening_players) == 0: return False else: #logger.info("There are some threatening players! Now shanten is {}".format(shanten)) pass # more than 2 players are threatening, so defense is better #if len(threatening_players) >= 2: # logger.info("Watch those players feed each other!") # return True if shanten == 1: # # When player is in 4th position, it's better to push in this situation # if self.player == self.table.get_players_sorted_by_scores()[-1]: # logger.info("Player is in 4th position, better to push.") # return False # TODO calculate all possible hand costs for 1-2 shanten dora_count = sum([ plus_dora(x, self.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.table.has_open_tanyao) ]) # we had 3+ dora in our almost done hand, # we can try to push it if dora_count >= 3: return False # our hand is not tempai, so better to fold it if shanten != 0: #logger.info("Not prepared, ready to fold.") return True # we are in tempai, if there are so many melds, don't go to defence # if len(self.player.melds) >= 2: # logger.info("Too many melds, should push.") # return False # we are in tempai, let's try to estimate hand value hands_estimated_cost = [] call_riichi = not self.player.is_open_hand for tile in waiting: # copy of tiles, because we are modifying a list tiles = self.player.tiles[:] # special case, when we already have 14 tiles in the hand if discard_candidate: temp_tile = discard_candidate.find_tile_in_hand( self.player.closed_hand) tiles.remove(temp_tile) hand_result = self.player.ai.estimate_hand_value( tile, tiles, call_riichi) if hand_result.error is None: hands_estimated_cost.append(hand_result.cost['main']) # probably we are with opened hand without yaku, let's fold it if not hands_estimated_cost: logger.info("This hand cannot win, fold it.") return True # Get the hand value hand_value = sum(hands_estimated_cost) / len(hands_estimated_cost) hand_value += self.table.count_of_riichi_sticks * 1000 if self.player.is_dealer: hand_value += 700 # EV for dealer combo # EH: makes the calculation of hand value better by adding the remaining tile count # Get the shape for attacking hand_shape = "bad_shape" if wanted_tiles_count > 4: hand_shape = "good_shape" # Check whether the player is in proactive mode if "PROACTIVE" in self.player.play_state: hand_shape = "pro_" + hand_shape # Get the current hand index hand_index = len(self.player.discards) # Get the type of threatening player counter_player_type = "player" if threatening_players[0].is_dealer: counter_player_type = "dealer" score_ev = hand_value - COUNTER_VALUES[ counter_player_type] * COUNTER_RATIO[hand_shape][hand_index] rank_ev = self.get_rank_ev(hand_value, COUNTER_VALUES[counter_player_type], COUNTER_RATIO[hand_shape][hand_index]) should_counter = False if self.table.round_number < 3: # DEBUG: set this to 0 to debug rank ev calculation # Before Round East 4, use score ev if score_ev > 0: should_counter = True else: if rank_ev > 0: should_counter = True elif rank_ev == 0 and score_ev > 0: should_encounter = True logger.info('''Cowboy: Counter: Hand Value: {} Hand Shape: {} Hand Index: {} Counter Player Type: {} Score EV: {} Rank EV: {} Should Counter: {}'''.format(hand_value, hand_shape, hand_index, counter_player_type, score_ev, rank_ev, should_counter)) if should_counter: # set state self.player.ai.waiting = waiting self.player.ai.wanted_tiles_count = wanted_tiles_count if self.player.play_state in ["PREPARING", "DEFENCE"]: if hand_shape == "good_shape": self.player.set_state("REACTIVE_GOODSHAPE") else: self.player.set_state("REACTIVE_BADSHAPE") if self.player != self.table.get_players_sorted_by_scores()[0]: # When player is on the top, no need to push, else push it self.player.ai.pushing = True return False else: return True # our open hand in tempai, but it is cheap # so we can fold it # if self.player.is_open_hand and max_cost < 7000: # return True # when we call riichi we can get ura dora, # so it is reasonable to riichi 3k+ hands # if not self.player.is_open_hand: # # there are a lot of chances that we will not win with a bad wait # # against other threatening players # if max_cost < 3000 or len(waiting) < 2: # return True return False
def estimate_hand_value(self, tiles, win_tile, is_tsumo=False, is_riichi=False, is_dealer=False, is_ippatsu=False, is_rinshan=False, is_chankan=False, is_haitei=False, is_houtei=False, is_daburu_riichi=False, is_nagashi_mangan=False, is_tenhou=False, is_renhou=False, is_chiihou=False, open_sets=None, dora_indicators=None, called_kan_indices=None, player_wind=None, round_wind=None): """ :param tiles: array with 14 tiles in 136-tile format :param win_tile: tile that caused win (ron or tsumo) :param is_tsumo: :param is_riichi: :param is_dealer: :param is_ippatsu: :param is_rinshan: :param is_chankan: :param is_haitei: :param is_houtei: :param is_tenhou: :param is_renhou: :param is_chiihou: :param is_daburu_riichi: :param is_nagashi_mangan: :param open_sets: array of array with open sets in 136-tile format :param dora_indicators: array of tiles in 136-tile format :param called_kan_indices: array of tiles in 136-tile format :param player_wind: index of player wind :param round_wind: index of round wind :return: The dictionary with hand cost or error response {"cost": {'main': 1000, 'additional': 0}, "han": 1, "fu": 30, "error": None, "hand_yaku": []} {"cost": None, "han": 0, "fu": 0, "error": "Hand is not valid", "hand_yaku": []} """ if not open_sets: open_sets = [] else: # cast 136 format to 34 format for item in open_sets: item[0] //= 4 item[1] //= 4 item[2] //= 4 is_open_hand = len(open_sets) > 0 if not dora_indicators: dora_indicators = [] kan_indices_136 = [] if not called_kan_indices: called_kan_indices = [] else: kan_indices_136 = called_kan_indices called_kan_indices = [x // 4 for x in called_kan_indices] agari = Agari() cost = None error = None hand_yaku = [] han = 0 fu = 0 def return_response(): return { 'cost': cost, 'error': error, 'han': han, 'fu': fu, 'hand_yaku': hand_yaku } # special situation if is_nagashi_mangan: hand_yaku.append(yaku.nagashi_mangan) fu = 30 han = yaku.nagashi_mangan.han['closed'] cost = self.calculate_scores(han, fu, is_tsumo, is_dealer) return return_response() if win_tile not in tiles: error = "Win tile not in the hand" return return_response() if is_riichi and is_open_hand: error = "Riichi can't be declared with open hand" return return_response() if is_ippatsu and is_open_hand: error = "Ippatsu can't be declared with open hand" return return_response() if is_ippatsu and not is_riichi and not is_daburu_riichi: error = "Ippatsu can't be declared without riichi" return return_response() tiles_34 = TilesConverter.to_34_array(tiles) divider = HandDivider() if not agari.is_agari(tiles_34): error = 'Hand is not winning' return return_response() hand_options = divider.divide_hand(tiles_34, open_sets, called_kan_indices) calculated_hands = [] for hand in hand_options: cost = None error = None hand_yaku = [] han = 0 fu = 0 if is_tsumo or is_open_hand: fu += 20 else: fu += 30 pon_sets = [x for x in hand if is_pon(x)] chi_sets = [x for x in hand if is_chi(x)] additional_fu = self.calculate_additional_fu( win_tile, hand, is_tsumo, player_wind, round_wind, open_sets, called_kan_indices) if additional_fu == 0 and len(chi_sets) == 4: """ - A hand without pon and kan sets, so it should contains all sequences and a pair - The pair should be not valued - The waiting must be an open wait (on 2 different tiles) - Hand should be closed """ if is_open_hand: fu += 2 is_pinfu = False else: is_pinfu = True else: fu += additional_fu is_pinfu = False if is_tsumo: if not is_open_hand: hand_yaku.append(yaku.tsumo) # pinfu + tsumo always is 20 fu if not is_pinfu: fu += 2 if is_pinfu: hand_yaku.append(yaku.pinfu) is_chitoitsu = self.is_chitoitsu(hand) # let's skip hand that looks like chitoitsu, but it contains open sets if is_chitoitsu and is_open_hand: continue if is_chitoitsu: hand_yaku.append(yaku.chiitoitsu) is_tanyao = self.is_tanyao(hand) if is_open_hand and not settings.OPEN_TANYAO: is_tanyao = False if is_tanyao: hand_yaku.append(yaku.tanyao) if is_riichi and not is_daburu_riichi: hand_yaku.append(yaku.riichi) if is_daburu_riichi: hand_yaku.append(yaku.daburu_riichi) if is_ippatsu: hand_yaku.append(yaku.ippatsu) if is_rinshan: hand_yaku.append(yaku.rinshan) if is_chankan: hand_yaku.append(yaku.chankan) if is_haitei: hand_yaku.append(yaku.haitei) if is_houtei: hand_yaku.append(yaku.houtei) if is_renhou: hand_yaku.append(yaku.renhou) if is_tenhou: hand_yaku.append(yaku.tenhou) if is_chiihou: hand_yaku.append(yaku.chiihou) if self.is_honitsu(hand): hand_yaku.append(yaku.honitsu) if self.is_chinitsu(hand): hand_yaku.append(yaku.chinitsu) if self.is_tsuisou(hand): hand_yaku.append(yaku.tsuisou) if self.is_honroto(hand): hand_yaku.append(yaku.honroto) if self.is_chinroto(hand): hand_yaku.append(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.is_chanta(hand): hand_yaku.append(yaku.chanta) if self.is_junchan(hand): hand_yaku.append(yaku.junchan) if self.is_ittsu(hand): hand_yaku.append(yaku.ittsu) if not is_open_hand: if self.is_ryanpeiko(hand): hand_yaku.append(yaku.ryanpeiko) elif self.is_iipeiko(hand): hand_yaku.append(yaku.iipeiko) if self.is_sanshoku(hand): hand_yaku.append(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.is_toitoi(hand): hand_yaku.append(yaku.toitoi) if self.is_sanankou(win_tile, hand, open_sets, is_tsumo): hand_yaku.append(yaku.sanankou) if self.is_sanshoku_douko(hand): hand_yaku.append(yaku.sanshoku_douko) if self.is_shosangen(hand): hand_yaku.append(yaku.shosangen) if self.is_haku(hand): hand_yaku.append(yaku.haku) if self.is_hatsu(hand): hand_yaku.append(yaku.hatsu) if self.is_chun(hand): hand_yaku.append(yaku.hatsu) if self.is_east(hand, player_wind, round_wind): if player_wind == EAST: hand_yaku.append(yaku.yakuhai_place) if round_wind == EAST: hand_yaku.append(yaku.yakuhai_round) if self.is_south(hand, player_wind, round_wind): if player_wind == SOUTH: hand_yaku.append(yaku.yakuhai_place) if round_wind == SOUTH: hand_yaku.append(yaku.yakuhai_round) if self.is_west(hand, player_wind, round_wind): if player_wind == WEST: hand_yaku.append(yaku.yakuhai_place) if round_wind == WEST: hand_yaku.append(yaku.yakuhai_round) if self.is_north(hand, player_wind, round_wind): if player_wind == NORTH: hand_yaku.append(yaku.yakuhai_place) if round_wind == NORTH: hand_yaku.append(yaku.yakuhai_round) if self.is_daisangen(hand): hand_yaku.append(yaku.daisangen) if self.is_shosuushi(hand): hand_yaku.append(yaku.shosuushi) if self.is_daisuushi(hand): hand_yaku.append(yaku.daisuushi) if self.is_ryuisou(hand): hand_yaku.append(yaku.ryuisou) if not is_open_hand and self.is_chuuren_poutou(hand): if tiles_34[win_tile // 4] == 2: hand_yaku.append(yaku.daburu_chuuren_poutou) else: hand_yaku.append(yaku.chuuren_poutou) if not is_open_hand and self.is_suuankou( win_tile, hand, is_tsumo): if tiles_34[win_tile // 4] == 2: hand_yaku.append(yaku.suuankou_tanki) else: hand_yaku.append(yaku.suuankou) if self.is_sankantsu(hand, called_kan_indices): hand_yaku.append(yaku.sankantsu) if self.is_suukantsu(hand, called_kan_indices): hand_yaku.append(yaku.suukantsu) # chitoitsu is always 25 fu if is_chitoitsu: fu = 25 tiles_for_dora = tiles + kan_indices_136 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): count_of_aka_dora += 1 if count_of_dora: yaku_item = yaku.dora yaku_item.han['open'] = count_of_dora yaku_item.han['closed'] = count_of_dora hand_yaku.append(yaku_item) if count_of_aka_dora: yaku_item = yaku.aka_dora yaku_item.han['open'] = count_of_aka_dora yaku_item.han['closed'] = count_of_aka_dora hand_yaku.append(yaku_item) # 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'] # round up # 22 -> 30 and etc. if fu != 25: fu = int(math.ceil(fu / 10.0)) * 10 if han == 0 or (han == 1 and fu < 30): error = 'Not valid han ({0}) and fu ({1})'.format(han, fu) cost = None else: cost = self.calculate_scores(han, fu, is_tsumo, is_dealer) calculated_hand = { 'cost': cost, 'error': error, 'hand_yaku': hand_yaku, 'han': han, 'fu': fu } calculated_hands.append(calculated_hand) # exception hand if not is_open_hand and self.is_kokushi(tiles_34): if tiles_34[win_tile // 4] == 2: han = yaku.daburu_kokushi.han['closed'] else: han = yaku.kokushi.han['closed'] fu = 0 cost = self.calculate_scores(han, fu, is_tsumo, is_dealer) calculated_hands.append({ 'cost': cost, 'error': None, 'hand_yaku': [yaku.kokushi], 'han': han, 'fu': fu }) # 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'] return return_response()
def should_activate_strategy(self): """ Tanyao hand is a hand without terminal and honor tiles, to achieve this we will use different approaches :return: boolean """ result = super(TanyaoStrategy, self).should_activate_strategy() if not result: return False # 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) ]) # Get shanten shanten = self.player.ai.previous_shanten # Get hand index hand_index = len(self.player.discards) #if not ((dora_count >= 1) or (shanten <= 1 and hand_index >= 8) or self.player.is_dealer): # return False if len(self.player.discards) <= 4: #Do not activate it too early return False tiles = TilesConverter.to_34_array(self.player.tiles) count_of_terminal_pon_sets = 0 count_of_terminal_pairs = 0 count_of_valued_pairs = 0 for x in range(0, 34): tile = tiles[x] if not tile: continue if x in self.not_suitable_tiles and tile == 3: count_of_terminal_pon_sets += 1 if x in self.not_suitable_tiles and tile == 2: count_of_terminal_pairs += 1 if x in self.player.valued_honors: count_of_valued_pairs += 1 # if we already have pon of honor\terminal tiles # we don't need to open hand for tanyao if count_of_terminal_pon_sets > 0: return False # with valued pair (yakuhai wind or dragon) # we don't need to go for tanyao if count_of_valued_pairs > 0: return False # no pair is ok in tanyao pair # but 1+ pairs can't be suitable #if count_of_terminal_pairs >= 1: # return False # 1234 and 9876 indices indices = [ [0, 1, 2, 3], [8, 7, 6, 5], [9, 10, 11, 12], [17, 16, 15, 14], [18, 19, 20, 21], [26, 25, 24, 23], ] num_terminal_lugs = 0 for index_set in indices: first = int(tiles[index_set[0]] >= 1) second = int(tiles[index_set[1]] >= 1) third = int(tiles[index_set[2]] >= 1) fourth = int(tiles[index_set[3]] >= 1) if ((first + second >= 2) or (first + third >= 2) or (second + third >= 2)) and fourth == 0: num_terminal_lugs += 1 if first + second + third >= 3: num_terminal_lugs += 2 num_terminal_lugs += count_of_terminal_pairs if num_terminal_lugs > 1: return False return True
def is_dora(self, tile): """ tile in 136 format """ return plus_dora(tile, self.dora_indicators) or is_aka_dora(tile, True)
def is_dora(self, tile): return plus_dora(tile, self.dora_indicators) or is_aka_dora(tile, self.has_open_tanyao)
def is_dora(self, tile): return plus_dora(tile, self.dora_indicators) or is_aka_dora(tile)
def calculate_tiles_danger( self, discard_candidates: List[DiscardOption], enemy_analyzer: EnemyAnalyzer ) -> List[DiscardOption]: closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) safe_against_threat_34 = [] # First, add all genbutsu to the list safe_against_threat_34.extend(list(set([x for x in enemy_analyzer.enemy.all_safe_tiles]))) # Then add tiles not suitable for yaku in enemy open hand if enemy_analyzer.threat_reason.get("active_yaku"): safe_against_yaku = set.intersection( *[set(x.get_safe_tiles_34()) for x in enemy_analyzer.threat_reason.get("active_yaku")] ) if safe_against_yaku: safe_against_threat_34.extend(list(safe_against_yaku)) possible_forms = self.possible_forms_analyzer.calculate_possible_forms(enemy_analyzer.enemy.all_safe_tiles) kabe_tiles = self.player.ai.kabe.find_all_kabe(closed_hand_34) suji_tiles = self.player.ai.suji.find_suji([x.value for x in enemy_analyzer.enemy.discards]) for discard_option in discard_candidates: tile_34 = discard_option.tile_to_discard_34 tile_136 = discard_option.find_tile_in_hand(self.player.closed_hand) number_of_revealed_tiles = self.player.number_of_revealed_tiles(tile_34, closed_hand_34) # like 1-9 against tanyao etc. if tile_34 in safe_against_threat_34: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.SAFE_AGAINST_THREATENING_HAND, ) continue # safe tiles that can be safe based on the table situation if self.total_possible_forms_for_tile(possible_forms, tile_34) == 0: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.IMPOSSIBLE_WAIT, ) continue # honors if is_honor(tile_34): danger = self._process_danger_for_honor(enemy_analyzer, tile_34, number_of_revealed_tiles) # terminals elif is_terminal(tile_34): danger = self._process_danger_for_terminal_tiles_and_kabe_suji( enemy_analyzer, tile_34, number_of_revealed_tiles, kabe_tiles, suji_tiles ) # 2-8 tiles else: danger = self._process_danger_for_2_8_tiles_suji_and_kabe( enemy_analyzer, tile_34, number_of_revealed_tiles, suji_tiles, kabe_tiles ) if danger: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, danger, ) forms_count = possible_forms[tile_34] self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, { "value": self.possible_forms_analyzer.calculate_possible_forms_danger(forms_count), "description": TileDanger.FORM_BONUS_DESCRIPTION, "forms_count": forms_count, }, ) # for ryanmen waits we also account for number of dangerous suji tiles forms_ryanmen_count = forms_count[PossibleFormsAnalyzer.POSSIBLE_RYANMEN_SIDES] if forms_ryanmen_count == 1: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.RYANMEN_BASE_SINGLE, ) elif forms_ryanmen_count == 2: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.RYANMEN_BASE_DOUBLE, ) if forms_ryanmen_count == 1 or forms_ryanmen_count == 2: has_matagi = self._is_matagi_suji(enemy_analyzer, tile_34) if has_matagi: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.BONUS_MATAGI_SUJI, can_be_used_for_ryanmen=True, ) has_aidayonken = self.is_aidayonken_pattern(enemy_analyzer, tile_34) if has_aidayonken: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.BONUS_AIDAYONKEN, can_be_used_for_ryanmen=True, ) early_danger_bonus = self._get_early_danger_bonus(enemy_analyzer, tile_34, has_matagi or has_aidayonken) if early_danger_bonus is not None: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, early_danger_bonus, can_be_used_for_ryanmen=True, ) self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.make_unverified_suji_coeff(enemy_analyzer.unverified_suji_coeff), can_be_used_for_ryanmen=True, ) if is_dora_connector(tile_136, self.player.table.dora_indicators): self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.DORA_CONNECTOR_BONUS, can_be_used_for_ryanmen=True, ) dora_count = plus_dora( tile_136, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora ) if dora_count > 0: danger = copy(TileDanger.DORA_BONUS) danger["value"] = dora_count * danger["value"] danger["dora_count"] = dora_count self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, danger, ) if enemy_analyzer.threat_reason.get("active_yaku"): for yaku_analyzer in enemy_analyzer.threat_reason.get("active_yaku"): bonus_danger = yaku_analyzer.get_bonus_danger(tile_136, number_of_revealed_tiles) for danger in bonus_danger: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, danger, ) return discard_candidates
def should_activate_strategy(self, tiles_136, meld_tile=None): """ We can go for honitsu strategy if we have prevalence of one suit and honor tiles """ result = super(HonitsuStrategy, self).should_activate_strategy(tiles_136) if not result: return False tiles_34 = TilesConverter.to_34_array(tiles_136) suits = count_tiles_by_suits(tiles_34) suits = [x for x in suits if x["name"] != "honor"] suits = sorted(suits, key=lambda x: x["count"], reverse=True) suit = suits[0] count_of_shuntsu_other_suits = 0 count_of_koutsu_other_suits = 0 count_of_ryanmen_other_suits = 0 count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[1]["function"]) count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[2]["function"]) count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[1]["function"]) count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[2]["function"]) count_of_ryanmen_other_suits += self._find_ryanmen_waits(tiles_34, suits[1]["function"]) count_of_ryanmen_other_suits += self._find_ryanmen_waits(tiles_34, suits[2]["function"]) self._calculate_suitable_and_not_suitable_tiles_cnt(tiles_34, suit["function"]) self._initialize_honitsu_dora_count(tiles_136, suit) # let's not go for honitsu if we have 5 or more tiles in other suits if self.tiles_count_other_suits >= 5: return False # 7th turn and still 4 tiles in other suits - meh if self.tiles_count_other_suits >= 4 and self.player.round_step > 6: return False # 12th turn is too late and we still have too many tiles in other suits if self.tiles_count_other_suits >= 3 and self.player.round_step > 11: return False # let's not go for honitsu if we have 2 or more non-isolated doras # in other suits if self.dora_count_other_suits_not_isolated >= 2: return False # if we have a pon of valued doras, let's not go for honitsu # we have a mangan anyway, let's go for fastest hand valued_pons = [x for x in self.player.valued_honors if tiles_34[x] >= 3] for pon in valued_pons: dora_count_valued_pons = plus_dora(pon * 4, self.player.table.dora_indicators) if dora_count_valued_pons > 0: return False valued_pairs = len([x for x in self.player.valued_honors if tiles_34[x] == 2]) honor_pairs_or_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2]) honor_doras_pairs_or_pons = len( [ x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2 and plus_dora(x * 4, self.player.table.dora_indicators) ] ) unvalued_singles = len( [x for x in range(0, 34) if is_honor(x) and x not in self.player.valued_honors and tiles_34[x] == 1] ) # what's the point of honitsu if there is not a single honor pair if honor_pairs_or_pons == 0: return False # if we have twu ryanmens in other suits if count_of_ryanmen_other_suits >= 2: return False # let's not go for honitsu nomi if not valued_pairs and not valued_pons: # this is not honitsu, maybe it will be pinfu one day if self.tiles_count_our_suit <= 7 and honor_pairs_or_pons < 2: return False # also looks more like pinfu if self.tiles_count_other_suits >= 4: return False # so-so, let's just not go for honitsu nomi if self.tiles_count_our_suit <= 9 and honor_pairs_or_pons == 1: if not self.dora_count_our_suit and not honor_doras_pairs_or_pons: return False # if we have some decent amount of not isolated tiles in other suits # we may not rush for honitsu considering other conditions if self.tiles_count_other_suits_not_isolated >= 3: # if we don't have pair or pon of honored doras if honor_doras_pairs_or_pons == 0: # if we have a ryanmen with dora in other suit and no honor doras, so let's not rush honitsu if count_of_ryanmen_other_suits >= 1 and self.dora_count_other_suits_not_isolated >= 1: return False # we need to either have a valued pair or have at least two honor # pairs to consider honitsu if valued_pairs == 0 and honor_pairs_or_pons < 2: return False # doesn't matter valued or not, if we have just one honor pair # and have some single unvalued tiles, let's throw them away # first if honor_pairs_or_pons == 1 and unvalued_singles >= 2: return False # 3 non-isolated unsuitable tiles, 1-shanen and already 8th turn # let's not consider honitsu here if self.player.ai.shanten == 1 and self.player.round_step > 8: return False else: # we have a pon of unvalued honor doras, but it looks like # it's faster to build our hand without honitsu if self.player.ai.shanten == 1: return False # if we have a complete set in other suits, we can only throw it away if it's early in the game if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1: # too late to throw away chi after 5 step if self.player.round_step > 5: return False # already 1 shanten, no need to throw away complete set if self.player.ai.shanten == 1: return False # dora is not isolated and we have a complete set, let's not go for honitsu if self.dora_count_other_suits_not_isolated >= 1: return False self.chosen_suit = suit["function"] return True
def should_go_to_defence_mode(self, discard_candidate=None): """ The method is decides should bot go to the defence mode or not. For now only full defence is possible :return: true|false """ # we drew a tile, so we have 14 tiles in our hand if discard_candidate: shanten = discard_candidate.shanten waiting = discard_candidate.waiting # we have 13 tiles in hand (this is not our turn) else: shanten = self.player.ai.shanten waiting = self.player.ai.waiting if not waiting: waiting = [] # if we are in riichi, we can't defence if self.player.in_riichi: return False threatening_players = self._get_threatening_players() # no one is threatening, so we can build our hand if len(threatening_players) == 0: return False if shanten == 1: # TODO calculate all possible hand costs for 1-2 shanten dora_count = sum([ plus_dora(x, self.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.table.has_open_tanyao) ]) # we had 3+ dora in our almost done hand, # we can try to push it if dora_count >= 3: return False # our hand is not tempai, so better to fold it if shanten != 0: return True # we are in tempai, let's try to estimate hand value hands_estimated_cost = [] call_riichi = not self.player.is_open_hand for tile in waiting: # copy of tiles, because we are modifying a list tiles = self.player.tiles[:] # special case, when we already have 14 tiles in the hand if discard_candidate: temp_tile = discard_candidate.find_tile_in_hand( self.player.closed_hand) tiles.remove(temp_tile) hand_result = self.player.ai.estimate_hand_value( tile, tiles, call_riichi) if hand_result.error is None: hands_estimated_cost.append(hand_result.cost['main']) # probably we are with opened hand without yaku, let's fold it if not hands_estimated_cost: return True max_cost = max(hands_estimated_cost) # our open hand in tempai, but it is cheap # so we can fold it if self.player.is_open_hand and max_cost < 7000: return True # when we call riichi we can get ura dora, # so it is reasonable to riichi 3k+ hands if not self.player.is_open_hand: # there are a lot of chances that we will not win with a bad wait # against other threatening players if max_cost < 3000 or len(waiting) < 2: return True return False
def collect_stat_for_enemy_riichi_hand_cost(tile_136, enemy, main_player): tile_34 = tile_136 // 4 riichi_discard = [x for x in enemy.discards if x.riichi_discard] if riichi_discard: assert len(riichi_discard) == 1 riichi_discard = riichi_discard[0] else: # FIXME: it happens when user called riichi and we are trying to decide to we need to open hand on # riichi tile or not. We need to process this situation correctly. riichi_discard = enemy.discards[-1] riichi_called_on_step = enemy.discards.index(riichi_discard) + 1 total_dora_in_game = len(enemy.table.dora_indicators) * 4 + ( 3 * int(enemy.table.has_aka_dora)) visible_tiles = enemy.table.revealed_tiles_136 + main_player.closed_hand visible_dora_tiles = sum([ plus_dora(x, enemy.table.dora_indicators, add_aka_dora=enemy.table.has_aka_dora) for x in visible_tiles ]) live_dora_tiles = total_dora_in_game - visible_dora_tiles assert live_dora_tiles >= 0, "Live dora tiles can't be less than 0" number_of_kan_in_enemy_hand = 0 number_of_dora_in_enemy_kan_sets = 0 number_of_yakuhai_enemy_kan_sets = 0 for meld in enemy.melds: # if he is in riichi he can only have closed kan assert meld.type == MeldPrint.KAN and not meld.opened number_of_kan_in_enemy_hand += 1 for tile in meld.tiles: number_of_dora_in_enemy_kan_sets += plus_dora( tile, enemy.table.dora_indicators, add_aka_dora=enemy.table.has_aka_dora) tile_meld_34 = meld.tiles[0] // 4 if tile_meld_34 in enemy.valued_honors: number_of_yakuhai_enemy_kan_sets += 1 number_of_other_player_kan_sets = 0 for other_player in enemy.table.players: if other_player.seat == enemy.seat: continue for meld in other_player.melds: if meld.type == MeldPrint.KAN or meld.type == MeldPrint.SHOUMINKAN: number_of_other_player_kan_sets += 1 tile_category = "" # additional danger for tiles that could be used for tanyao if not is_honor(tile_34): # +1 here to make it more readable simplified_tile = simplify(tile_34) + 1 if simplified_tile in [4, 5, 6]: tile_category = "middle" if simplified_tile in [2, 3, 7, 8]: tile_category = "edge" if simplified_tile in [1, 9]: tile_category = "terminal" else: tile_category = "honor" if tile_34 in enemy.valued_honors: tile_category = "valuable_honor" return { "is_dealer": enemy.is_dealer and 1 or 0, "riichi_called_on_step": riichi_called_on_step, "current_enemy_step": len(enemy.discards), "wind_number": main_player.table.round_wind_number, "scores": enemy.scores, "is_tsumogiri_riichi": riichi_discard.is_tsumogiri and 1 or 0, "is_oikake_riichi": enemy.is_oikake_riichi and 1 or 0, "is_oikake_riichi_against_dealer_riichi_threat": enemy.is_oikake_riichi_against_dealer_riichi_threat and 1 or 0, "is_riichi_against_open_hand_threat": enemy.is_riichi_against_open_hand_threat and 1 or 0, "number_of_kan_in_enemy_hand": number_of_kan_in_enemy_hand, "number_of_dora_in_enemy_kan_sets": number_of_dora_in_enemy_kan_sets, "number_of_yakuhai_enemy_kan_sets": number_of_yakuhai_enemy_kan_sets, "number_of_other_player_kan_sets": number_of_other_player_kan_sets, "live_dora_tiles": live_dora_tiles, "tile_plus_dora": plus_dora(tile_136, enemy.table.dora_indicators, add_aka_dora=enemy.table.has_aka_dora), "tile_category": tile_category, "discards_before_riichi_34": ";".join([ str(x.value // 4) for x in enemy.discards[:riichi_called_on_step] ]), }
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)
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 is_threatening(self) -> bool: """ We are trying to determine other players current threat """ round_step = len(self.enemy.discards) if self.enemy.in_riichi: self._create_danger_reason(EnemyDanger.THREAT_RIICHI, round_step=round_step) return True melds = self.enemy.melds # we can't analyze closed hands for now if not melds: return False active_yaku = [] sure_han = 0 yakuhai_analyzer = YakuhaiAnalyzer(self.enemy) if yakuhai_analyzer.is_yaku_active(): active_yaku.append(yakuhai_analyzer) sure_han = yakuhai_analyzer.melds_han() yaku_analyzers = [ ChinitsuAnalyzer(self.enemy), HonitsuAnalyzer(self.enemy), ToitoiAnalyzer(self.enemy), TanyaoAnalyzer(self.enemy), ] for x in yaku_analyzers: if x.is_yaku_active(): active_yaku.append(x) if not active_yaku: active_yaku.append(AtodzukeAnalyzer(self.enemy)) sure_han = 1 # FIXME: probably our approach here should be refactored and we should not care about cost if not sure_han: main_yaku = [ x for x in active_yaku if not x.is_absorbed(active_yaku) ] if main_yaku: sure_han = main_yaku[0].melds_han() else: sure_han = 1 meld_tiles = self.enemy.meld_tiles dora_count = sum([ plus_dora(x, self.table.dora_indicators, add_aka_dora=self.table.has_aka_dora) for x in meld_tiles ]) sure_han += dora_count if len(melds) == 1 and round_step > 5 and sure_han >= 4: self._create_danger_reason( EnemyDanger.THREAT_OPEN_HAND_AND_MULTIPLE_DORA, melds, dora_count, active_yaku, round_step) return True if len(melds) >= 2 and round_step > 4 and sure_han >= 3: self._create_danger_reason(EnemyDanger.THREAT_EXPENSIVE_OPEN_HAND, melds, dora_count, active_yaku, round_step) return True if len( melds ) >= 1 and round_step > 10 and sure_han >= 2 and self.enemy.is_dealer: self._create_danger_reason( EnemyDanger.THREAT_OPEN_HAND_UNKNOWN_COST, melds, dora_count, active_yaku, round_step) return True # we are not sure how expensive this is, but let's be a little bit careful if (round_step > 14 and len(melds) >= 1) or ( round_step > 9 and len(melds) >= 2) or len(melds) >= 3: self._create_danger_reason( EnemyDanger.THREAT_OPEN_HAND_UNKNOWN_COST, melds, dora_count, active_yaku, round_step) return True return False
def prepare_closed_hand_input( round_wind, dora_indicators, player_closed_hand, player_melds, player_discards, tenpai_player_wind, tenpai_player_riichi, tenpai_player_melds, tenpai_player_discards, second_player_melds, second_player_discards, third_player_melds, third_player_discards, ): """ meld dict format = { 'tiles': List of 136 format tiles } discard dict format = { 'tile': 136 format, 'is_tsumogiri': bool, 'is_after_meld': bool, } :param round_wind: 27 (east), 28 (south), 29 (west), 30 (north) :param dora_indicators: List of 136 format tiles :param player_closed_hand: Our player. List of 136 format tiles :param player_melds: Our player. List of meld dicts :param player_discards: Our player. List of discard dicts :param tenpai_player_wind: 27 (east), 28 (south), 29 (west), 30 (north) :param tenpai_player_riichi: bool :param tenpai_player_melds: Tenpai player. List of meld dicts :param tenpai_player_discards: Tenpai player. List of discard dicts :param second_player_melds: Second player. List of meld dicts :param second_player_discards: Second player. List of discard dicts :param third_player_melds: Third player. List of meld dicts :param third_player_discards: Third player. List of discard dicts :return: list of 0 and 1 values """ tiles_unique = 34 max_dora_in_hand = 8 max_dora_on_the_table = 16 + 3 winds_input_size = 8 tenpai_player_discards_input = [0 for _ in range(tiles_unique)] tenpai_player_melds_input = [1 for _ in range(tiles_unique)] winds_input = [0 for _ in range(winds_input_size)] dora_in_player_open_melds_input = [0 for _ in range(max_dora_in_hand)] not_visible_dora_on_the_table_input = [ 0 for _ in range(max_dora_on_the_table) ] for discard_dict in tenpai_player_discards: tile = discard_dict['tile'] // 4 tenpai_player_discards_input[tile] = 1 for meld in tenpai_player_melds: tiles = meld['tiles'] for tile in tiles: tile = tile // 4 tenpai_player_melds_input[tile] = 1 out_tiles_136 = [] for tile_136 in player_closed_hand: out_tiles_136.append(tile_136) for tile_136 in dora_indicators: out_tiles_136.append(tile_136) discards = [ player_discards, tenpai_player_discards, second_player_discards, third_player_discards ] for discards_list in discards: for x in discards_list: # we will add this tile in melds loop if x['was_taken_for_meld']: continue out_tiles_136.append(x['tile']) melds = [ player_melds, tenpai_player_melds, second_player_melds, third_player_melds ] for meld_list in melds: for x in meld_list: out_tiles_136.extend(x['tiles']) out_tiles = [0 for _ in range(tiles_unique)] for x in out_tiles_136: tile = x // 4 out_tiles[tile] += 1 assert out_tiles[tile] <= 4 out_tiles_0 = [1 if x >= 1 else 0 for x in out_tiles] out_tiles_1 = [1 if x >= 2 else 0 for x in out_tiles] out_tiles_2 = [1 if x >= 3 else 0 for x in out_tiles] out_tiles_3 = [1 if x == 4 else 0 for x in out_tiles] if round_wind == EAST: winds_input[0] = 1 elif round_wind == SOUTH: winds_input[1] = 1 elif round_wind == WEST: winds_input[2] = 1 elif round_wind == NORTH: winds_input[3] = 1 if tenpai_player_wind == EAST: winds_input[4] = 1 elif tenpai_player_wind == SOUTH: winds_input[5] = 1 elif tenpai_player_wind == WEST: winds_input[6] = 1 elif tenpai_player_wind == NORTH: winds_input[7] = 1 number_of_dora_in_player_open_melds = 0 player_melds = tenpai_player_melds for meld in player_melds: for tile in meld['tiles']: number_of_dora_in_player_open_melds += plus_dora( tile, dora_indicators) if is_aka_dora(tile, True): number_of_dora_in_player_open_melds += 1 if number_of_dora_in_player_open_melds > max_dora_in_hand: number_of_dora_in_player_open_melds = max_dora_in_hand for i in range(max_dora_in_hand): if i + 1 <= number_of_dora_in_player_open_melds: dora_in_player_open_melds_input[i] = 1 visible_dora = 0 for visible_tile in out_tiles: visible_dora += plus_dora(visible_tile, dora_indicators) if is_aka_dora(visible_tile, True): visible_dora += 1 not_visible_dora = max_dora_on_the_table - visible_dora for i in range(max_dora_on_the_table): if i + 1 <= not_visible_dora: not_visible_dora_on_the_table_input[i] = 1 return list( itertools.chain( winds_input, not_visible_dora_on_the_table_input, dora_in_player_open_melds_input, tenpai_player_discards_input, tenpai_player_melds_input, out_tiles_0, out_tiles_1, out_tiles_2, out_tiles_3, ))
def should_activate_strategy(self, tiles_136): """ We can go for chinitsu strategy if we have prevalence of one suit """ result = super(ChinitsuStrategy, self).should_activate_strategy(tiles_136) if not result: return False # when making decisions about chinitsu, we should consider # the state of our own hand, tiles_34 = TilesConverter.to_34_array(self.player.tiles) suits = count_tiles_by_suits(tiles_34) suits = [x for x in suits if x['name'] != 'honor'] suits = sorted(suits, key=lambda x: x['count'], reverse=True) suit = suits[0] count_of_shuntsu_other_suits = 0 count_of_koutsu_other_suits = 0 count_of_shuntsu_other_suits += HonitsuStrategy._count_of_shuntsu(tiles_34, suits[1]['function']) count_of_shuntsu_other_suits += HonitsuStrategy._count_of_shuntsu(tiles_34, suits[2]['function']) count_of_koutsu_other_suits += HonitsuStrategy._count_of_koutsu(tiles_34, suits[1]['function']) count_of_koutsu_other_suits += HonitsuStrategy._count_of_koutsu(tiles_34, suits[2]['function']) # we need to have at least 9 tiles of one suit to fo for chinitsu if suit['count'] < 9: return False # here we only check doras in different suits, we will deal # with honors later self._initialize_chinitsu_dora_count(tiles_136, suit) # 3 non-isolated doras in other suits is too much # to even try if self.dora_count_not_suitable >= 3: return False if self.dora_count_not_suitable == 2: # 2 doras in other suits, no doras in our suit # let's not consider chinitsu if self.dora_count_suitable == 0: return False # we have 2 doras in other suits and we # are 1 shanten, let's not rush chinitsu if self.player.ai.shanten == 1: return False # too late to get rid of doras in other suits if self.player.round_step > 8: return False # we are almost tempai, chinitsu is slower if suit['count'] == 9 and self.player.ai.shanten == 1: return False # only 10 tiles by 8th turn is too slow, considering alternative if suit['count'] == 10 and self.player.ai.shanten == 1 and self.player.round_step > 8: return False # if we have a pon of honors, let's not go for chinitsu honor_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 3]) if honor_pons >= 1: return False # if we have a valued pair, let's not go for chinitsu valued_pairs = len([x for x in self.player.valued_honors if tiles_34[x] == 2]) if valued_pairs >= 1: return False # if we have a pair of honor doras, let's not go for chinitsu honor_doras_pairs = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] == 2 and plus_dora(x * 4, self.player.table.dora_indicators)]) if honor_doras_pairs >= 1: return False # if we have a honor pair, we will only throw them away if it's early in the game # and if we have lots of tiles in our suit honor_pairs = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] == 2]) if honor_pairs >= 2: return False if honor_pairs == 1: if suit['count'] < 11: return False if self.player.round_step > 8: return False # if we have a complete set in other suits, we can only throw it away if it's early in the game if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1: # too late to throw away chi after 8 step if self.player.round_step > 8: return False # already 1 shanten, no need to throw away complete set if self.player.round_step > 5 and self.player.ai.shanten == 1: return False # dora is not isolated and we have a complete set, let's not go for chinitsu if self.dora_count_not_suitable >= 1: return False self.chosen_suit = suit['function'] return True
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 is_dora(self, tile): return plus_dora(tile, self.dora_indicators, add_aka_dora=self.has_aka_dora)
def calculate_value(self): # base is 100 for ability to mark tiles as not needed (like set value to 50) value = 100 honored_value = 20 if is_honor(self.tile_to_discard): if self.tile_to_discard in self.player.valued_honors: count_of_winds = [x for x in self.player.valued_honors if x == self.tile_to_discard] # for west-west, east-east we had to double tile value value += honored_value * len(count_of_winds) else: # aim for tanyao if self.player.ai.current_strategy and self.player.ai.current_strategy.type == BaseStrategy.TANYAO: suit_tile_grades = [10, 20, 30, 50, 40, 50, 30, 20, 10] # usual hand else: suit_tile_grades = [10, 20, 40, 50, 30, 50, 40, 20, 10] simplified_tile = simplify(self.tile_to_discard) value += suit_tile_grades[simplified_tile] for indicator in self.player.table.dora_indicators: indicator_34 = indicator // 4 if is_honor(indicator_34): continue # indicator and tile not from the same suit if is_sou(indicator_34) and not is_sou(self.tile_to_discard): continue # indicator and tile not from the same suit if is_man(indicator_34) and not is_man(self.tile_to_discard): continue # indicator and tile not from the same suit if is_pin(indicator_34) and not is_pin(self.tile_to_discard): continue simplified_indicator = simplify(indicator_34) simplified_dora = simplified_indicator + 1 # indicator is 9 man if simplified_dora == 9: simplified_dora = 0 # tile so close to the dora if simplified_tile + 1 == simplified_dora or simplified_tile - 1 == simplified_dora: value += DiscardOption.DORA_FIRST_NEIGHBOUR # tile not far away from dora if simplified_tile + 2 == simplified_dora or simplified_tile - 2 == simplified_dora: value += DiscardOption.DORA_SECOND_NEIGHBOUR count_of_dora = plus_dora(self.tile_to_discard * 4, self.player.table.dora_indicators) tile_136 = self.find_tile_in_hand(self.player.closed_hand) if is_aka_dora(tile_136, self.player.table.has_aka_dora): count_of_dora += 1 self.count_of_dora = count_of_dora value += count_of_dora * DiscardOption.DORA_VALUE if is_honor(self.tile_to_discard): # depends on how much honor tiles were discarded # we will decrease tile value discard_percentage = [100, 75, 20, 0, 0] discarded_tiles = self.player.table.revealed_tiles[self.tile_to_discard] value = (value * discard_percentage[discarded_tiles]) / 100 # three honor tiles were discarded, # so we don't need this tile anymore if value == 0: self.had_to_be_discarded = True self.valuation = int(value)
def calculate_danger_borders(self, discard_options, threatening_player, all_threatening_players): min_shanten = min([x.shanten for x in discard_options]) placement_adjustment = self.player.ai.placement.get_allowed_danger_modifier() for discard_option in discard_options: danger_border = DangerBorder.BETAORI hand_weighted_cost = 0 tune = 0 shanten = discard_option.shanten tile_136 = discard_option.tile_to_discard_136 if discard_option.danger.get_total_danger_for_player(threatening_player.enemy.seat) == 0: threatening_player_hand_cost = 0 else: threatening_player_hand_cost = threatening_player.get_assumed_hand_cost( tile_136, discard_option.danger.can_be_used_for_ryanmen ) # fast path: we don't need to calculate all the stuff if this tile is safe against this enemy if threatening_player_hand_cost == 0: discard_option.danger.set_danger_border( threatening_player.enemy.seat, DangerBorder.IGNORE, hand_weighted_cost, threatening_player_hand_cost ) continue if discard_option.shanten == 0: hand_weighted_cost = self.player.ai.estimate_weighted_mean_hand_value(discard_option) # we are not ready to push with hand that doesn't have chances to win # or to get ryukoku payments if hand_weighted_cost == 0: discard_option.danger.set_danger_border( threatening_player.enemy.seat, DangerBorder.BETAORI, hand_weighted_cost, threatening_player_hand_cost, ) continue discard_option.danger.weighted_cost = hand_weighted_cost cost_ratio = (hand_weighted_cost / threatening_player_hand_cost) * 100 tune = self.player.config.TUNE_DANGER_BORDER_TEMPAI_VALUE if self.player.ai.placement.must_push( all_threatening_players, discard_option.tile_to_discard_136, num_shanten=0, tempai_cost=hand_weighted_cost, ): danger_border = DangerBorder.IGNORE else: # good wait if discard_option.ukeire >= 6: if cost_ratio >= 100: danger_border = DangerBorder.IGNORE elif cost_ratio >= 70: danger_border = DangerBorder.VERY_HIGH elif cost_ratio >= 50: danger_border = DangerBorder.UPPER_MEDIUM elif cost_ratio >= 30: danger_border = DangerBorder.MEDIUM else: danger_border = DangerBorder.LOW # moderate wait elif discard_option.ukeire >= 4: if cost_ratio >= 400: danger_border = DangerBorder.IGNORE elif cost_ratio >= 200: danger_border = DangerBorder.EXTREME elif cost_ratio >= 100: danger_border = DangerBorder.VERY_HIGH elif cost_ratio >= 70: danger_border = DangerBorder.UPPER_MEDIUM elif cost_ratio >= 50: danger_border = DangerBorder.LOWER_MEDIUM elif cost_ratio >= 30: danger_border = DangerBorder.UPPER_LOW else: danger_border = DangerBorder.VERY_LOW # weak wait elif discard_option.ukeire >= 2: if cost_ratio >= 400: danger_border = DangerBorder.EXTREME elif cost_ratio >= 200: danger_border = DangerBorder.VERY_HIGH elif cost_ratio >= 100: danger_border = DangerBorder.UPPER_MEDIUM elif cost_ratio >= 70: danger_border = DangerBorder.MEDIUM elif cost_ratio >= 50: danger_border = DangerBorder.UPPER_LOW elif cost_ratio >= 30: danger_border = DangerBorder.LOW else: danger_border = DangerBorder.EXTREMELY_LOW # waiting for 1 tile basically else: if cost_ratio >= 400: danger_border = DangerBorder.HIGH elif cost_ratio >= 200: danger_border = DangerBorder.UPPER_MEDIUM elif cost_ratio >= 100: danger_border = DangerBorder.LOWER_MEDIUM elif cost_ratio >= 50: danger_border = DangerBorder.LOW else: danger_border = DangerBorder.EXTREMELY_LOW if discard_option.shanten == 1: tune = self.player.config.TUNE_DANGER_BORDER_1_SHANTEN_VALUE # FIXME: temporary solution to avoid too much ukeire2 calculation if min_shanten == 0: hand_weighted_cost = 2000 else: hand_weighted_cost = discard_option.average_second_level_cost # never push with zero chance to win # FIXME: we may actually want to push it for tempai in ryukoku, so reconsider if not hand_weighted_cost: discard_option.danger.set_danger_border( threatening_player.enemy.seat, DangerBorder.BETAORI, hand_weighted_cost, threatening_player_hand_cost, ) continue discard_option.danger.weighted_cost = int(hand_weighted_cost) cost_ratio = (hand_weighted_cost / threatening_player_hand_cost) * 100 average_tempai_waits = discard_option.average_second_level_waits if self.player.ai.placement.must_push( all_threatening_players, discard_option.tile_to_discard_136, num_shanten=1, tempai_cost=hand_weighted_cost, ): danger_border = DangerBorder.IGNORE else: # lots of ukeire if discard_option.ukeire >= 32 and average_tempai_waits >= 6: if cost_ratio >= 400: danger_border = DangerBorder.IGNORE elif cost_ratio >= 200: danger_border = DangerBorder.EXTREME elif cost_ratio >= 100: danger_border = DangerBorder.VERY_HIGH elif cost_ratio >= 50: danger_border = DangerBorder.MEDIUM elif cost_ratio >= 20: danger_border = DangerBorder.UPPER_LOW else: danger_border = DangerBorder.EXTREMELY_LOW # very good ukeire elif discard_option.ukeire >= 20 and average_tempai_waits >= 6: if cost_ratio >= 400: danger_border = DangerBorder.IGNORE elif cost_ratio >= 200: danger_border = DangerBorder.EXTREME elif cost_ratio >= 100: danger_border = DangerBorder.VERY_HIGH elif cost_ratio >= 50: danger_border = DangerBorder.LOWER_MEDIUM elif cost_ratio >= 20: danger_border = DangerBorder.LOW else: danger_border = DangerBorder.EXTREMELY_LOW # good ukeire elif discard_option.ukeire >= 12 and average_tempai_waits >= 4: if cost_ratio >= 400: danger_border = DangerBorder.VERY_HIGH elif cost_ratio >= 200: danger_border = DangerBorder.HIGH elif cost_ratio >= 100: danger_border = DangerBorder.UPPER_MEDIUM elif cost_ratio >= 50: danger_border = DangerBorder.UPPER_LOW elif cost_ratio >= 20: danger_border = DangerBorder.VERY_LOW else: danger_border = DangerBorder.BETAORI # mediocre ukeire elif discard_option.ukeire >= 7 and average_tempai_waits >= 2: if cost_ratio >= 400: danger_border = DangerBorder.HIGH elif cost_ratio >= 200: danger_border = DangerBorder.UPPER_MEDIUM elif cost_ratio >= 100: danger_border = DangerBorder.LOWER_MEDIUM elif cost_ratio >= 50: danger_border = DangerBorder.VERY_LOW elif cost_ratio >= 20: danger_border = DangerBorder.LOWEST else: danger_border = DangerBorder.BETAORI # very low ukeire elif discard_option.ukeire >= 3 and average_tempai_waits >= 1: if cost_ratio >= 400: danger_border = DangerBorder.MEDIUM elif cost_ratio >= 200: danger_border = DangerBorder.UPPER_LOW elif cost_ratio >= 100: danger_border = DangerBorder.VERY_LOW elif cost_ratio >= 50: danger_border = DangerBorder.LOWEST else: danger_border = DangerBorder.BETAORI # little to no ukeire else: danger_border = DangerBorder.BETAORI if discard_option.shanten == 2: tune = self.player.config.TUNE_DANGER_BORDER_2_SHANTEN_VALUE if self.player.is_dealer: scale = [0, 1000, 2900, 5800, 7700, 12000, 18000, 18000, 24000, 24000, 48000] else: scale = [0, 1000, 2000, 3900, 5200, 8000, 12000, 12000, 16000, 16000, 32000] if self.player.is_open_hand: # FIXME: each strategy should have a han value, we should use it instead han = 1 else: # TODO: try to estimate yaku chances for closed hand han = 1 dora_count = sum( [ plus_dora(x, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora) for x in self.player.tiles ] ) han += dora_count hand_weighted_cost = scale[min(han, len(scale) - 1)] discard_option.danger.weighted_cost = int(hand_weighted_cost) cost_ratio = (hand_weighted_cost / threatening_player_hand_cost) * 100 if self.player.ai.placement.must_push( all_threatening_players, discard_option.tile_to_discard_136, num_shanten=2, tempai_cost=hand_weighted_cost, ): danger_border = DangerBorder.IGNORE else: # lots of ukeire if discard_option.ukeire >= 40: if cost_ratio >= 400: danger_border = DangerBorder.HIGH elif cost_ratio >= 200: danger_border = DangerBorder.MEDIUM elif cost_ratio >= 100: danger_border = DangerBorder.EXTREMELY_LOW else: danger_border = DangerBorder.BETAORI # very good ukeire elif discard_option.ukeire >= 20: if cost_ratio >= 400: danger_border = DangerBorder.UPPER_MEDIUM elif cost_ratio >= 200: danger_border = DangerBorder.LOW elif cost_ratio >= 100: danger_border = DangerBorder.LOWEST else: danger_border = DangerBorder.BETAORI # mediocre ukeire or worse else: danger_border = DangerBorder.BETAORI # if we could have chosen tempai, pushing 1 or more shanten is usually # a pretty bad idea, so tune down if discard_option.shanten != 0 and min_shanten == 0: danger_border = DangerBorder.tune_down(danger_border, 2) # depending on our placement we may want to be more defensive or more offensive tune += placement_adjustment danger_border = DangerBorder.tune(danger_border, tune) # if it's late there are generally less reasons to be aggressive danger_border = DangerBorder.tune_for_round(self.player, danger_border, shanten) discard_option.danger.set_danger_border( threatening_player.enemy.seat, danger_border, hand_weighted_cost, threatening_player_hand_cost ) return discard_options
def should_activate_strategy(self, tiles_136, meld_tile=None): """ We can go for chinitsu strategy if we have prevalence of one suit """ result = super(ChinitsuStrategy, self).should_activate_strategy(tiles_136) if not result: return False # when making decisions about chinitsu, we should consider # the state of our own hand, tiles_34 = TilesConverter.to_34_array(self.player.tiles) suits = count_tiles_by_suits(tiles_34) suits = [x for x in suits if x["name"] != "honor"] suits = sorted(suits, key=lambda x: x["count"], reverse=True) suit = suits[0] count_of_shuntsu_other_suits = 0 count_of_koutsu_other_suits = 0 count_of_shuntsu_other_suits += HonitsuStrategy._count_of_shuntsu( tiles_34, suits[1]["function"]) count_of_shuntsu_other_suits += HonitsuStrategy._count_of_shuntsu( tiles_34, suits[2]["function"]) count_of_koutsu_other_suits += HonitsuStrategy._count_of_koutsu( tiles_34, suits[1]["function"]) count_of_koutsu_other_suits += HonitsuStrategy._count_of_koutsu( tiles_34, suits[2]["function"]) # we need to have at least 9 tiles of one suit to fo for chinitsu if suit["count"] < 9: return False # here we only check doras in different suits, we will deal # with honors later self._initialize_chinitsu_dora_count(tiles_136, suit) # 3 non-isolated doras in other suits is too much # to even try if self.dora_count_not_suitable >= 3: return False if self.dora_count_not_suitable == 2: # 2 doras in other suits, no doras in our suit # let's not consider chinitsu if self.dora_count_suitable == 0: return False # we have 2 doras in other suits and we # are 1 shanten, let's not rush chinitsu if self.player.ai.shanten == 1: return False # too late to get rid of doras in other suits if self.player.round_step > 8: return False # we are almost tempai, chinitsu is slower if suit["count"] == 9 and self.player.ai.shanten == 1: return False # only 10 tiles by 9th turn is too slow, considering alternative if suit["count"] == 10 and self.player.ai.shanten == 1 and self.player.round_step > 8: return False # only 11 tiles or less by 12th turn is too slow, considering alternative if suit["count"] <= 11 and self.player.round_step > 11: return False # if we have a pon of honors, let's not go for chinitsu honor_pons = len( [x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 3]) if honor_pons >= 1: return False # if we have a valued pair, let's not go for chinitsu valued_pairs = len( [x for x in self.player.valued_honors if tiles_34[x] == 2]) if valued_pairs >= 1: return False # if we have a pair of honor doras, let's not go for chinitsu honor_doras_pairs = len([ x for x in range(0, 34) if is_honor(x) and tiles_34[x] == 2 and plus_dora(x * 4, self.player.table.dora_indicators) ]) if honor_doras_pairs >= 1: return False # if we have a honor pair, we will only throw them away if it's early in the game # and if we have lots of tiles in our suit honor_pairs = len( [x for x in range(0, 34) if is_honor(x) and tiles_34[x] == 2]) if honor_pairs >= 2: return False if honor_pairs == 1: if suit["count"] < 11: return False if self.player.round_step > 8: return False # if we have a complete set in other suits, we can only throw it away if it's early in the game if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1: # too late to throw away chi after 8 step if self.player.round_step > 8: return False # already 1 shanten, no need to throw away complete set if self.player.round_step > 5 and self.player.ai.shanten == 1: return False # dora is not isolated and we have a complete set, let's not go for chinitsu if self.dora_count_not_suitable >= 1: return False self.chosen_suit = suit["function"] return True
def _should_call_riichi_one_sided(self, waiting): count_tiles = self.player.ai.hand_builder.count_tiles( waiting, TilesConverter.to_34_array(self.player.closed_hand)) waiting = waiting[0] hand_value = self.player.ai.estimate_hand_value_or_get_from_cache( waiting, call_riichi=False) hand_value_with_riichi = self.player.ai.estimate_hand_value_or_get_from_cache( waiting, call_riichi=True) must_riichi = self.player.ai.placement.must_riichi( has_yaku=(hand_value.yaku is not None and hand_value.cost is not None), num_waits=count_tiles, cost_with_riichi=hand_value_with_riichi.cost["main"], cost_with_damaten=(hand_value.cost and hand_value.cost["main"] or 0), ) if must_riichi == Placement.MUST_RIICHI: return True elif must_riichi == Placement.MUST_DAMATEN: return False tiles = self.player.closed_hand[:] closed_melds = [x for x in self.player.melds if not x.opened] for meld in closed_melds: tiles.extend(meld.tiles[:3]) results, tiles_34 = self.player.ai.hand_builder.divide_hand( tiles, waiting) result = results[0] closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) have_suji, have_kabe = self.player.ai.hand_builder.check_suji_and_kabe( closed_tiles_34, waiting) # what if we have yaku if hand_value.yaku is not None and hand_value.cost is not None: min_cost = hand_value.cost["main"] # tanki honor is a good wait, let's damaten only if hand is already expensive if is_honor(waiting): if self.player.is_dealer and min_cost < 12000: return True if not self.player.is_dealer and min_cost < 8000: return True return False is_chiitoitsu = len([x for x in result if is_pair(x)]) == 7 simplified_waiting = simplify(waiting) for hand_set in result: if waiting not in hand_set: continue # tanki wait but not chiitoitsu if is_pair(hand_set) and not is_chiitoitsu: # let's not riichi tanki 4, 5, 6 if 3 <= simplified_waiting <= 5: return False # don't riichi tanki wait on 1, 2, 3, 7, 8, 9 if it's only 1 tile if count_tiles == 1: return False # don't riichi 2378 tanki if hand has good value if simplified_waiting != 0 and simplified_waiting != 8: if self.player.is_dealer and min_cost >= 7700: return False if not self.player.is_dealer and min_cost >= 5200: return False # only riichi if we have suji-trab or there is kabe if not have_suji and not have_kabe: return False return True # tanki wait with chiitoitsu if is_pair(hand_set) and is_chiitoitsu: # chiitoitsu on last suit tile is not the best if count_tiles == 1: return False # early riichi on 19 tanki is good if (simplified_waiting == 0 or simplified_waiting == 8) and self.player.round_step < 7: return True # riichi on 19 tanki is good later too if we have 3 tiles to wait for if ((simplified_waiting == 0 or simplified_waiting == 8) and self.player.round_step < 12 and count_tiles == 3): return True # riichi on 28 tanki is good if we have 3 tiles to wait for if ((simplified_waiting == 1 or simplified_waiting == 7) and self.player.round_step < 12 and count_tiles == 3): return True # otherwise only riichi if we have suji-trab or there is kabe if not have_suji and not have_kabe: return False return True # 1-sided wait means kanchan or penchan if is_chi(hand_set): # let's not riichi kanchan on 4, 5, 6 if 3 <= simplified_waiting <= 5: return False # now checking waiting for 2, 3, 7, 8 # if we only have 1 tile to wait for, let's damaten if count_tiles == 1: return False # if we have 2 tiles to wait for and hand cost is good without riichi, # let's damaten if count_tiles == 2: if self.player.is_dealer and min_cost >= 7700: return False if not self.player.is_dealer and min_cost >= 5200: return False # if we have more than two tiles to wait for and we have kabe or suji - insta riichi if count_tiles > 2 and (have_suji or have_kabe): return True # 2 and 8 are good waits but not in every condition if simplified_waiting == 1 or simplified_waiting == 7: if self.player.round_step < 7: if self.player.is_dealer and min_cost < 18000: return True if not self.player.is_dealer and min_cost < 8000: return True if self.player.round_step < 12: if self.player.is_dealer and min_cost < 12000: return True if not self.player.is_dealer and min_cost < 5200: return True if self.player.round_step < 15: if self.player.is_dealer and 2000 < min_cost < 7700: return True # 3 and 7 are ok waits sometimes too if simplified_waiting == 2 or simplified_waiting == 6: if self.player.round_step < 7: if self.player.is_dealer and min_cost < 12000: return True if not self.player.is_dealer and min_cost < 5200: return True if self.player.round_step < 12: if self.player.is_dealer and min_cost < 7700: return True if not self.player.is_dealer and min_cost < 5200: return True if self.player.round_step < 15: if self.player.is_dealer and 2000 < min_cost < 7700: return True # otherwise only riichi if we have suji-trab or there is kabe if not have_suji and not have_kabe: return False return True # what if we don't have yaku # our tanki wait is good, let's riichi if is_honor(waiting): return True if count_tiles > 1: # terminal tanki is ok, too, just should be more than one tile left if is_terminal(waiting): return True # whatever dora wait is ok, too, just should be more than one tile left if plus_dora(waiting * 4, self.player.table.dora_indicators, add_aka_dora=False) > 0: return True simplified_waiting = simplify(waiting) for hand_set in result: if waiting not in hand_set: continue if is_pair(hand_set): # let's not riichi tanki wait without suji-trap or kabe if not have_suji and not have_kabe: return False # let's not riichi tanki on last suit tile if it's early if count_tiles == 1 and self.player.round_step < 6: return False # let's not riichi tanki 4, 5, 6 if it's early if 3 <= simplified_waiting <= 5 and self.player.round_step < 6: return False # 1-sided wait means kanchan or penchan # let's only riichi this bad wait if # it has all 4 tiles available or it # it's not too early if is_chi(hand_set) and 4 <= simplified_waiting <= 6: return count_tiles == 4 or self.player.round_step >= 6 return True
def _calculate_assumed_hand_cost_for_riichi( self, tile_136, can_be_used_for_ryanmen) -> int: scale_index = 0 tile_34 = tile_136 // 4 if self.enemy.is_dealer: scale = [ 2900, 5800, 7700, 12000, 12000, 18000, 18000, 24000, 24000, 48000 ] else: scale = [ 2000, 3900, 5200, 8000, 8000, 12000, 12000, 16000, 16000, 32000 ] # it wasn't early riichi, let's think that it could be more expensive if 6 <= self.enemy.riichi_called_on_step <= 11: scale_index += 1 # more late riichi, probably means more expensive riichi if self.enemy.riichi_called_on_step >= 12: scale_index += 2 if self.enemy.is_ippatsu: scale_index += 1 total_dora_in_game = len(self.table.dora_indicators) * 4 + ( 3 * int(self.table.has_aka_dora)) visible_tiles = self.table.revealed_tiles_136 + self.main_player.closed_hand visible_dora_tiles = sum([ plus_dora(x, self.table.dora_indicators, add_aka_dora=self.table.has_aka_dora) for x in visible_tiles ]) live_dora_tiles = total_dora_in_game - visible_dora_tiles assert live_dora_tiles >= 0, "Live dora tiles can't be less than 0" # there are too many live dora tiles, let's increase hand cost if live_dora_tiles >= 4: scale_index += 1 # if we are discarding dora we are obviously going to make enemy hand more expensive scale_index += self._get_dora_scale_bonus(tile_136) # if enemy has closed kan, his hand is more expensive on average for meld in self.enemy.melds: # if he is in riichi he can only have closed kan assert meld.type == Meld.KAN and not meld.opened # plus two just because of riichi with kan scale_index += 2 # higher danger for doras for tile in meld.tiles: scale_index += plus_dora(tile, self.table.dora_indicators, add_aka_dora=self.table.has_aka_dora) # higher danger for yakuhai tile_meld_34 = meld.tiles[0] // 4 scale_index += len( [x for x in self.enemy.valued_honors if x == tile_meld_34]) # let's add more danger for all other opened kan sets on the table for other_player in self.table.players: if other_player.seat == self.enemy.seat: continue for meld in other_player.melds: if meld.type == Meld.KAN or meld.type == Meld.SHOUMINKAN: scale_index += 1 # additional danger for tiles that could be used for tanyao if not is_honor(tile_34): # +1 here to make it more readable simplified_tile = simplify(tile_34) + 1 if simplified_tile in [4, 5, 6]: scale_index += 1 if simplified_tile in [2, 3, 7, 8] and can_be_used_for_ryanmen: scale_index += 1 if scale_index > len(scale) - 1: scale_index = len(scale) - 1 return scale[scale_index]
def calculate_valuation(self): # base is 100 for ability to mark tiles as not needed (like set value to 50) value = 100 honored_value = 20 if is_honor(self.tile_to_discard_34): if self.tile_to_discard_34 in self.player.valued_honors: count_of_winds = [ x for x in self.player.valued_honors if x == self.tile_to_discard_34 ] # for west-west, east-east we had to double tile value value += honored_value * len(count_of_winds) else: # aim for tanyao if self.player.ai.current_strategy and self.player.ai.current_strategy.type == BaseStrategy.TANYAO: suit_tile_grades = [10, 20, 30, 50, 40, 50, 30, 20, 10] # usual hand else: suit_tile_grades = [10, 20, 40, 50, 30, 50, 40, 20, 10] simplified_tile = simplify(self.tile_to_discard_34) value += suit_tile_grades[simplified_tile] for indicator in self.player.table.dora_indicators: indicator_34 = indicator // 4 if is_honor(indicator_34): continue # indicator and tile not from the same suit if is_sou(indicator_34) and not is_sou( self.tile_to_discard_34): continue # indicator and tile not from the same suit if is_man(indicator_34) and not is_man( self.tile_to_discard_34): continue # indicator and tile not from the same suit if is_pin(indicator_34) and not is_pin( self.tile_to_discard_34): continue simplified_indicator = simplify(indicator_34) simplified_dora = simplified_indicator + 1 # indicator is 9 man if simplified_dora == 9: simplified_dora = 0 # tile so close to the dora if simplified_tile + 1 == simplified_dora or simplified_tile - 1 == simplified_dora: value += DiscardOption.DORA_FIRST_NEIGHBOUR # tile not far away from dora if simplified_tile + 2 == simplified_dora or simplified_tile - 2 == simplified_dora: value += DiscardOption.DORA_SECOND_NEIGHBOUR count_of_dora = plus_dora(self.tile_to_discard_136, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora) self.count_of_dora = count_of_dora value += count_of_dora * DiscardOption.DORA_VALUE if is_honor(self.tile_to_discard_34): # depends on how much honor tiles were discarded # we will decrease tile value discard_percentage = [100, 75, 20, 0, 0] discarded_tiles = self.player.table.revealed_tiles[ self.tile_to_discard_34] value = (value * discard_percentage[discarded_tiles]) / 100 # three honor tiles were discarded, # so we don't need this tile anymore if value == 0: self.had_to_be_discarded = True self.valuation = int(value)
def should_go_to_defence_mode(self, discard_candidate=None): """ The method is decides should bot go to the defence mode or not. For now only full defence is possible :return: true|false """ # we drew a tile, so we have 14 tiles in our hand if discard_candidate: shanten = discard_candidate.shanten waiting = discard_candidate.waiting # we have 13 tiles in hand (this is not our turn) else: shanten = self.player.ai.shanten waiting = self.player.ai.waiting if not waiting: waiting = [] # if we are in riichi, we can't defence if self.player.in_riichi: return False threatening_players = self._get_threatening_players() # no one is threatening, so we can build our hand if len(threatening_players) == 0: return False if shanten == 1: # TODO calculate all possible hand costs for 1-2 shanten dora_count = sum([plus_dora(x, self.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.table.has_open_tanyao)]) # we had 3+ dora in our almost done hand, # we can try to push it if dora_count >= 3: return False # our hand is not tempai, so better to fold it if shanten != 0: return True # we are in tempai, let's try to estimate hand value hands_estimated_cost = [] call_riichi = not self.player.is_open_hand for tile in waiting: # copy of tiles, because we are modifying a list tiles = self.player.tiles[:] # special case, when we already have 14 tiles in the hand if discard_candidate: temp_tile = discard_candidate.find_tile_in_hand(self.player.closed_hand) tiles.remove(temp_tile) hand_result = self.player.ai.estimate_hand_value(tile, tiles, call_riichi) if hand_result.error is None: hands_estimated_cost.append(hand_result.cost['main']) # probably we are with opened hand without yaku, let's fold it if not hands_estimated_cost: return True max_cost = max(hands_estimated_cost) # our open hand in tempai, but it is cheap # so we can fold it if self.player.is_open_hand and max_cost < 7000: return True # when we call riichi we can get ura dora, # so it is reasonable to riichi 3k+ hands if not self.player.is_open_hand: # there are a lot of chances that we will not win with a bad wait # against other threatening players if max_cost < 3000 or len(waiting) < 2: return True return False