def is_condition_met(self, hand, player_wind, round_wind, *args): if len([x for x in hand if is_pon(x) and x[0] == player_wind]) == 1 and player_wind == EAST: return True if len([x for x in hand if is_pon(x) and x[0] == round_wind]) == 1 and round_wind == EAST: return True return False
def is_condition_met(self, hand, win_tile, is_tsumo): win_tile //= 4 closed_hand = [] for item in hand: # if we do the ron on syanpon wait our pon will be consider as open if is_pon(item) and win_tile in item and not is_tsumo: continue closed_hand.append(item) count_of_pon = len([i for i in closed_hand if is_pon(i)]) return count_of_pon == 4
def is_chun(self, hand): """ Pon of red dragons :param hand: list of hand's sets :return: true|false """ return len([x for x in hand if is_pon(x) and x[0] == CHUN]) == 1
def is_hatsu(self, hand): """ Pon of green dragons :param hand: list of hand's sets :return: true|false """ return len([x for x in hand if is_pon(x) and x[0] == HATSU]) == 1
def is_sanshoku_douko(self, hand): """ Three pon sets consisting of the same numbers in all three suits :param hand: list of hand's sets :return: true|false """ pon_sets = [i for i in hand if is_pon(i)] if len(pon_sets) < 3: return False sou_pon = [] pin_pon = [] man_pon = [] for item in pon_sets: if is_sou(item[0]): sou_pon.append(item) elif is_pin(item[0]): pin_pon.append(item) elif is_man(item[0]): man_pon.append(item) for sou_item in sou_pon: for pin_item in pin_pon: for man_item in man_pon: # cast tile indices to 1..9 representation sou_item = [simplify(x) for x in sou_item] pin_item = [simplify(x) for x in pin_item] man_item = [simplify(x) for x in man_item] if sou_item == pin_item == man_item: return True return False
def is_condition_met(self, hand, *args): pon_sets = [x for x in hand if is_pon(x)] if len(pon_sets) < 3: return False count_of_wind_sets = 0 wind_pair = 0 winds = [EAST, SOUTH, WEST, NORTH] for item in hand: if is_pon(item) and item[0] in winds: count_of_wind_sets += 1 if is_pair(item) and item[0] in winds: wind_pair += 1 return count_of_wind_sets == 3 and wind_pair == 1
def is_condition_met(self, hand, *args): pon_sets = [i for i in hand if is_pon(i)] if len(pon_sets) < 3: return False sou_pon = [] pin_pon = [] man_pon = [] for item in pon_sets: if is_sou(item[0]): sou_pon.append(item) elif is_pin(item[0]): pin_pon.append(item) elif is_man(item[0]): man_pon.append(item) for sou_item in sou_pon: for pin_item in pin_pon: for man_item in man_pon: # cast tile indices to 1..9 representation sou_item = [simplify(x) for x in sou_item] pin_item = [simplify(x) for x in pin_item] man_item = [simplify(x) for x in man_item] if sou_item == pin_item == man_item: return True return False
def is_condition_met(self, hand, win_tile, melds, is_tsumo): """ Three closed pon sets, the other sets need not to be closed :param hand: list of hand's sets :param win_tile: 136 tiles format :param melds: list Meld objects :param is_tsumo: :return: true|false """ win_tile //= 4 open_sets = [x.tiles_34 for x in melds if x.opened] chi_sets = [x for x in hand if (is_chi(x) and win_tile in x and x not in open_sets)] pon_sets = [x for x in hand if is_pon(x)] closed_pon_sets = [] for item in pon_sets: if item in open_sets: continue # if we do the ron on syanpon wait our pon will be consider as open # and it is not 789999 set if win_tile in item and not is_tsumo and not len(chi_sets): continue closed_pon_sets.append(item) return len(closed_pon_sets) == 3
def get_possible_meld(self, tile, is_kamicha_discard): closed_hand = self.player.closed_hand[:] # we can't open hand anymore if len(closed_hand) == 1: return None, None discarded_tile = tile // 4 closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile]) combinations = [] first_index = 0 second_index = 0 if is_man(discarded_tile): first_index = 0 second_index = 8 elif is_pin(discarded_tile): first_index = 9 second_index = 17 elif is_sou(discarded_tile): first_index = 18 second_index = 26 if second_index == 0: # honor tiles if closed_hand_34[discarded_tile] == 3: combinations = [[[discarded_tile] * 3]] else: # to avoid not necessary calculations # we can check only tiles around +-2 discarded tile first_limit = discarded_tile - 2 if first_limit < first_index: first_limit = first_index second_limit = discarded_tile + 2 if second_limit > second_index: second_limit = second_index combinations = self.hand_divider.find_valid_combinations( closed_hand_34, first_limit, second_limit, True) if combinations: combinations = combinations[0] # possible_melds = [] melds_chi, melds_pon = [], [] for best_meld_34 in combinations: # we can call pon from everyone if is_pon(best_meld_34) and discarded_tile in best_meld_34: if best_meld_34 not in melds_pon: melds_pon.append(best_meld_34) # we can call chi only from left player if is_chi( best_meld_34 ) and is_kamicha_discard and discarded_tile in best_meld_34: if best_meld_34 not in melds_chi: melds_chi.append(best_meld_34) return melds_chi, melds_pon
def is_toitoi(self, hand): """ The hand consists of all pon sets (and of course a pair), no sequences. :param hand: list of hand's sets :return: true|false """ count_of_pon = len([i for i in hand if is_pon(i)]) return count_of_pon == 4
def is_daisuushi(self, hand): """ The hand contains four sets of winds :param hand: list of hand's sets :return: true|false """ pon_sets = [x for x in hand if is_pon(x)] if len(pon_sets) != 4: return False count_wind_sets = 0 winds = [EAST, SOUTH, WEST, NORTH] for item in pon_sets: if is_pon(item) and item[0] in winds: count_wind_sets += 1 return count_wind_sets == 4
def is_valid_combination(possible_set): if is_chi(possible_set): return True if is_pon(possible_set): return True return False
def is_condition_met(self, hand, *args): """ The hand contains four sets of winds :param hand: list of hand's sets :return: boolean """ pon_sets = [x for x in hand if is_pon(x)] if len(pon_sets) != 4: return False count_wind_sets = 0 winds = [EAST, SOUTH, WEST, NORTH] for item in pon_sets: if is_pon(item) and item[0] in winds: count_wind_sets += 1 return count_wind_sets == 4
def is_south(self, hand, player_wind, round_wind): """ Pon of south winds :param hand: list of hand's sets :param player_wind: index of player wind :param round_wind: index of round wind :return: true|false """ if len([x for x in hand if is_pon(x) and x[0] == player_wind ]) == 1 and player_wind == SOUTH: return True if len([x for x in hand if is_pon(x) and x[0] == round_wind ]) == 1 and round_wind == SOUTH: return True return False
def is_condition_met(self, hand, *args): dragons = [CHUN, HAKU, HATSU] count_of_conditions = 0 for item in hand: # dragon pon or pair if (is_pair(item) or is_pon(item)) and item[0] in dragons: count_of_conditions += 1 return count_of_conditions == 3
def is_suuankou(self, win_tile, hand, is_tsumo): """ Four closed pon sets :param win_tile: 136 tiles format :param hand: list of hand's sets :param is_tsumo: :return: true|false """ win_tile //= 4 closed_hand = [] for item in hand: # if we do the ron on syanpon wait our pon will be consider as open if is_pon(item) and win_tile in item and not is_tsumo: continue closed_hand.append(item) count_of_pon = len([i for i in closed_hand if is_pon(i)]) return count_of_pon == 4
def is_daisangen(self, hand): """ The hand contains three sets of dragons :param hand: list of hand's sets :return: true|false """ count_of_dragon_pon_sets = 0 for item in hand: if is_pon(item) and item[0] in [CHUN, HAKU, HATSU]: count_of_dragon_pon_sets += 1 return count_of_dragon_pon_sets == 3
def is_shosuushi(self, hand): """ The hand contains three sets of winds and a pair of the remaining wind. :param hand: list of hand's sets :return: true|false """ pon_sets = [x for x in hand if is_pon(x)] if len(pon_sets) < 3: return False count_of_wind_sets = 0 wind_pair = 0 winds = [EAST, SOUTH, WEST, NORTH] for item in hand: if is_pon(item) and item[0] in winds: count_of_wind_sets += 1 if is_pair(item) and item[0] in winds: wind_pair += 1 return count_of_wind_sets == 3 and wind_pair == 1
def should_call_kan(self, tile, open_kan): """ When bot can call kan or chankan this method will be called :param tile: 136 tile format :param is_open_kan: boolean :return: kan type (Meld.KAN, Meld.CHANKAN) or None """ if open_kan: # don't start open hand from called kan if not self.player.is_open_hand: return None # don't call open kan if not waiting for win if not self.player.in_tempai: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # upgrade open pon to kan if possible if pon_melds: for meld in pon_melds: if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # check for improvement in shanten if new_shanten <= previous_shanten: return Meld.KAN return None
def is_shosangen(self, hand): """ Hand with two dragon pon sets and one dragon pair :param hand: list of hand's sets :return: true|false """ dragons = [CHUN, HAKU, HATSU] count_of_conditions = 0 for item in hand: # dragon pon or pair if (is_pair(item) or is_pon(item)) and item[0] in dragons: count_of_conditions += 1 return count_of_conditions == 3
def is_sankantsu(self, hand, called_kan_indices): """ The hand with three kan sets :param hand: list of hand's sets :param called_kan_indices: array of 34 tiles format :return: true|false """ if len(called_kan_indices) != 3: return False pon_sets = [i for i in hand if is_pon(i)] count_of_kan_sets = 0 for item in pon_sets: if item[0] in called_kan_indices: count_of_kan_sets += 1 return count_of_kan_sets == 3
def is_condition_met(self, hand, *args): count_of_pon = len([i for i in hand if is_pon(i)]) return count_of_pon == 4
def should_call_kan(self, tile, open_kan): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :return: kan type """ # we don't need to add dora for other players if self.player.ai.in_defence: return None if open_kan: # we don't want to start open our hand from called kan if not self.player.is_open_hand: return None # there is no sense to call open kan when we are not in tempai if not self.player.in_tempai: return None # we have a bad wait, rinshan chance is low if len(self.waiting) < 2: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # let's check can we upgrade opened pon to the kan if pon_melds: for meld in pon_melds: # tile is equal to our already opened pon, # so let's call chankan! if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 # we have 3 tiles in our hand, # so we can try to call closed meld if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # called kan will not ruin our hand if new_shanten <= previous_shanten: return Meld.KAN return None
def is_condition_met(self, hand, *args): return len([x for x in hand if is_pon(x) and x[0] == HATSU]) == 1
def boxes_is_pon(boxes): items = [t[0] for t in boxes[0:3]] return is_pon(items)
def calculate_fu(self, hand, win_tile, win_group, config, valued_tiles=None, melds=None,): """ Calculate hand fu with explanations :param hand: :param win_tile: 136 tile format :param win_group: one set where win tile exists :param is_tsumo: :param config: HandConfig object :param valued_tiles: dragons, player wind, round wind :param melds: opened sets :return: """ win_tile_34 = win_tile // 4 if not valued_tiles: valued_tiles = [] if not melds: melds = [] fu_details = [] if len(hand) == 7: return [{'fu': 25, 'reason': FuCalculator.BASE}], 25 pair = [x for x in hand if is_pair(x)][0] pon_sets = [x for x in hand if is_pon(x)] copied_opened_melds = [x.tiles_34 for x in melds if x.type == Meld.CHI] closed_chi_sets = [] for x in hand: if x not in copied_opened_melds: closed_chi_sets.append(x) else: copied_opened_melds.remove(x) is_open_hand = any([x.opened for x in melds]) if win_group in closed_chi_sets: tile_index = simplify(win_tile_34) # penchan if contains_terminals(win_group): # 1-2-... wait if tile_index == 2 and win_group.index(win_tile_34) == 2: fu_details.append({'fu': 2, 'reason': FuCalculator.PENCHAN}) # 8-9-... wait elif tile_index == 6 and win_group.index(win_tile_34) == 0: fu_details.append({'fu': 2, 'reason': FuCalculator.PENCHAN}) # kanchan waiting 5-...-7 if win_group.index(win_tile_34) == 1: fu_details.append({'fu': 2, 'reason': FuCalculator.KANCHAN}) # valued pair count_of_valued_pairs = valued_tiles.count(pair[0]) if count_of_valued_pairs == 1: fu_details.append({'fu': 2, 'reason': FuCalculator.VALUED_PAIR}) # east-east pair when you are on east gave double fu if count_of_valued_pairs == 2: fu_details.append({'fu': 2, 'reason': FuCalculator.VALUED_PAIR}) fu_details.append({'fu': 2, 'reason': FuCalculator.VALUED_PAIR}) # pair wait if is_pair(win_group): fu_details.append({'fu': 2, 'reason': FuCalculator.PAIR_WAIT}) for set_item in pon_sets: open_meld = [x for x in melds if set_item == x.tiles_34] open_meld = open_meld and open_meld[0] or None set_was_open = open_meld and open_meld.opened or False is_kan = (open_meld and (open_meld.type == Meld.KAN or open_meld.type == Meld.CHANKAN)) or False is_honor = set_item[0] in TERMINAL_INDICES + HONOR_INDICES # we win by ron on the third pon tile, our pon will be count as open if not config.is_tsumo and set_item == win_group: set_was_open = True if is_honor: if is_kan: if set_was_open: fu_details.append({'fu': 16, 'reason': FuCalculator.OPEN_TERMINAL_KAN}) else: fu_details.append({'fu': 32, 'reason': FuCalculator.CLOSED_TERMINAL_KAN}) else: if set_was_open: fu_details.append({'fu': 4, 'reason': FuCalculator.OPEN_TERMINAL_PON}) else: fu_details.append({'fu': 8, 'reason': FuCalculator.CLOSED_TERMINAL_PON}) else: if is_kan: if set_was_open: fu_details.append({'fu': 8, 'reason': FuCalculator.OPEN_KAN}) else: fu_details.append({'fu': 16, 'reason': FuCalculator.CLOSED_KAN}) else: if set_was_open: fu_details.append({'fu': 2, 'reason': FuCalculator.OPEN_PON}) else: fu_details.append({'fu': 4, 'reason': FuCalculator.CLOSED_PON}) add_tsumo_fu = len(fu_details) > 0 or config.fu_for_pinfu_tsumo if config.is_tsumo and add_tsumo_fu: # 2 additional fu for tsumo (but not for pinfu) fu_details.append({'fu': 2, 'reason': FuCalculator.TSUMO}) if is_open_hand and not len(fu_details) and config.fu_for_open_pinfu: # there is no 1-20 hands, so we had to add additional fu fu_details.append({'fu': 2, 'reason': FuCalculator.HAND_WITHOUT_FU}) if is_open_hand or config.is_tsumo: fu_details.append({'fu': 20, 'reason': FuCalculator.BASE}) else: fu_details.append({'fu': 30, 'reason': FuCalculator.BASE}) return fu_details, self.round_fu(fu_details)
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 try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): """ Determine should we call a meld or not. If yes, it will return Meld object and tile to discard :param tile: 136 format tile :param is_kamicha_discard: boolean :param new_tiles: :return: Meld and DiscardOption objects """ if self.player.in_riichi: return None, None if self.player.ai.in_defence: return None, None closed_hand = self.player.closed_hand[:] # we can't open hand anymore if len(closed_hand) == 1: return None, None # we can't use this tile for our chosen strategy if not self.is_tile_suitable(tile): return None, None discarded_tile = tile // 4 closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile]) combinations = [] first_index = 0 second_index = 0 if is_man(discarded_tile): first_index = 0 second_index = 8 elif is_pin(discarded_tile): first_index = 9 second_index = 17 elif is_sou(discarded_tile): first_index = 18 second_index = 26 if second_index == 0: # honor tiles if closed_hand_34[discarded_tile] == 3: combinations = [[[discarded_tile] * 3]] else: # to avoid not necessary calculations # we can check only tiles around +-2 discarded tile first_limit = discarded_tile - 2 if first_limit < first_index: first_limit = first_index second_limit = discarded_tile + 2 if second_limit > second_index: second_limit = second_index combinations = self.player.ai.hand_divider.find_valid_combinations( closed_hand_34, first_limit, second_limit, True ) if combinations: combinations = combinations[0] possible_melds = [] for best_meld_34 in combinations: # we can call pon from everyone if is_pon(best_meld_34) and discarded_tile in best_meld_34: if best_meld_34 not in possible_melds: possible_melds.append(best_meld_34) # we can call chi only from left player if is_chi(best_meld_34) and is_kamicha_discard and discarded_tile in best_meld_34: if best_meld_34 not in possible_melds: possible_melds.append(best_meld_34) # we can call melds only with allowed tiles validated_melds = [] for meld in possible_melds: if (self.is_tile_suitable(meld[0] * 4) and self.is_tile_suitable(meld[1] * 4) and self.is_tile_suitable(meld[2] * 4)): validated_melds.append(meld) possible_melds = validated_melds if not possible_melds: return None, None chosen_meld = self._find_best_meld_to_open(possible_melds, new_tiles, closed_hand, tile) selected_tile = chosen_meld['discard_tile'] meld = chosen_meld['meld'] shanten = selected_tile.shanten had_to_be_called = self.meld_had_to_be_called(tile) had_to_be_called = had_to_be_called or selected_tile.had_to_be_discarded # each strategy can use their own value to min shanten number if shanten > self.min_shanten: return None, None # sometimes we had to call tile, even if it will not improve our hand # otherwise we can call only with improvements of shanten if not had_to_be_called and shanten >= self.player.ai.shanten: return None, None return meld, selected_tile
def should_call_kan(self, tile, open_kan, from_riichi=False): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :param from_riichi: boolean :return: kan type """ # we can't call kan on the latest tile if self.table.count_of_remaining_tiles <= 1: return None # we don't need to add dora for other players if self.player.ai.in_defence: return None if open_kan: # we don't want to start open our hand from called kan if not self.player.is_open_hand: return None # there is no sense to call open kan when we are not in tempai if not self.player.in_tempai: return None # we have a bad wait, rinshan chance is low if len(self.waiting) < 2: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) melds_34 = copy.copy(self.player.meld_34_tiles) tiles = copy.copy(self.player.tiles) closed_hand_tiles = copy.copy(self.player.closed_hand) new_shanten = 0 previous_shanten = 0 new_waits_count = 0 previous_waits_count = 0 # let's check can we upgrade opened pon to the kan pon_melds = [x for x in self.player.meld_34_tiles if is_pon(x)] has_shouminkan_candidate = False for meld in pon_melds: # tile is equal to our already opened pon if tile_34 in meld: has_shouminkan_candidate = True tiles.append(tile) closed_hand_tiles.append(tile) previous_shanten, previous_waits_count = self._calculate_shanten_for_kan( tiles, closed_hand_tiles, self.player.melds) tiles_34 = TilesConverter.to_34_array(tiles) tiles_34[tile_34] -= 1 new_waiting, new_shanten = self.hand_builder.calculate_waits( tiles_34, self.player.meld_34_tiles) new_waits_count = self.hand_builder.count_tiles( new_waiting, tiles_34) if not has_shouminkan_candidate: # we don't have enough tiles in the hand if closed_hand_34[tile_34] != 3: return None if open_kan or from_riichi: # this 4 tiles can only be used in kan, no other options previous_waiting, previous_shanten = self.hand_builder.calculate_waits( tiles_34, melds_34) previous_waits_count = self.hand_builder.count_tiles( previous_waiting, closed_hand_34) else: tiles.append(tile) closed_hand_tiles.append(tile) previous_shanten, previous_waits_count = self._calculate_shanten_for_kan( tiles, closed_hand_tiles, self.player.melds) # shanten calculator doesn't like working with kans, so we pretend it's a pon melds_34 += [[tile_34, tile_34, tile_34]] new_waiting, new_shanten = self.hand_builder.calculate_waits( tiles_34, melds_34) closed_hand_34[tile_34] = 4 new_waits_count = self.hand_builder.count_tiles( new_waiting, closed_hand_34) # it is possible that we don't have results here # when we are in agari state (but without yaku) if previous_shanten is None: return None # it is not possible to reduce number of shanten by calling a kan assert new_shanten >= previous_shanten # if shanten number is the same, we should only call kan if ukeire didn't become worse if new_shanten == previous_shanten: # we cannot improve ukeire by calling kan (not considering the tile we drew from the dead wall) assert new_waits_count <= previous_waits_count if new_waits_count == previous_waits_count: return has_shouminkan_candidate and Meld.CHANKAN or Meld.KAN return None
def try_to_call_meld(self, tile, is_kamicha_discard): """ Determine should we call a meld or not. If yes, it will return Meld object and tile to discard :param tile: 136 format tile :param is_kamicha_discard: boolean :return: Meld and DiscardOption objects """ if self.player.in_riichi: return None, None if self.player.ai.in_defence: return None, None closed_hand = self.player.closed_hand[:] # we can't open hand anymore if len(closed_hand) == 1: return None, None # we can't use this tile for our chosen strategy if not self.is_tile_suitable(tile): return None, None discarded_tile = tile // 4 new_tiles = self.player.tiles[:] + [tile] closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile]) combinations = [] first_index = 0 second_index = 0 if is_man(discarded_tile): first_index = 0 second_index = 8 elif is_pin(discarded_tile): first_index = 9 second_index = 17 elif is_sou(discarded_tile): first_index = 18 second_index = 26 if second_index == 0: # honor tiles if closed_hand_34[discarded_tile] == 3: combinations = [[[discarded_tile] * 3]] else: # to avoid not necessary calculations # we can check only tiles around +-2 discarded tile first_limit = discarded_tile - 2 if first_limit < first_index: first_limit = first_index second_limit = discarded_tile + 2 if second_limit > second_index: second_limit = second_index combinations = self.player.ai.hand_divider.find_valid_combinations(closed_hand_34, first_limit, second_limit, True) if combinations: combinations = combinations[0] possible_melds = [] for best_meld_34 in combinations: # we can call pon from everyone if is_pon(best_meld_34) and discarded_tile in best_meld_34: if best_meld_34 not in possible_melds: possible_melds.append(best_meld_34) # we can call chi only from left player if is_chi(best_meld_34) and is_kamicha_discard and discarded_tile in best_meld_34: if best_meld_34 not in possible_melds: possible_melds.append(best_meld_34) # we can call melds only with allowed tiles validated_melds = [] for meld in possible_melds: if (self.is_tile_suitable(meld[0] * 4) and self.is_tile_suitable(meld[1] * 4) and self.is_tile_suitable(meld[2] * 4)): validated_melds.append(meld) possible_melds = validated_melds if not possible_melds: return None, None best_meld_34 = self._find_best_meld_to_open(possible_melds, new_tiles) if best_meld_34: # we need to calculate count of shanten with supposed meld # to prevent bad hand openings melds = self.player.open_hand_34_tiles + [best_meld_34] outs_results, shanten = self.player.ai.calculate_outs(new_tiles, closed_hand, melds) # each strategy can use their own value to min shanten number if shanten > self.min_shanten: return None, None # we can't improve hand, so we don't need to open it if not outs_results: return None, None # sometimes we had to call tile, even if it will not improve our hand # otherwise we can call only with improvements of shanten if not self.meld_had_to_be_called(tile) and shanten >= self.player.ai.previous_shanten: return None, None meld_type = is_chi(best_meld_34) and Meld.CHI or Meld.PON best_meld_34.remove(discarded_tile) first_tile = TilesConverter.find_34_tile_in_136_array(best_meld_34[0], closed_hand) closed_hand.remove(first_tile) second_tile = TilesConverter.find_34_tile_in_136_array(best_meld_34[1], closed_hand) closed_hand.remove(second_tile) tiles = [ first_tile, second_tile, tile ] meld = Meld() meld.type = meld_type meld.tiles = sorted(tiles) # we had to be sure that all our discard results exists in the closed hand filtered_results = [] for result in outs_results: if result.find_tile_in_hand(closed_hand): filtered_results.append(result) # we can't discard anything, so let's not open our hand if not filtered_results: return None, None selected_tile = self.player.ai.process_discard_options_and_select_tile_to_discard( filtered_results, shanten, had_was_open=True ) return meld, selected_tile return None, None
def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): """ Determine should we call a meld or not. If yes, it will return MeldPrint object and tile to discard :param tile: 136 format tile :param is_kamicha_discard: boolean :param new_tiles: :return: MeldPrint and DiscardOption objects """ if self.player.in_riichi: return None, None closed_hand = self.player.closed_hand[:] # we can't open hand anymore if len(closed_hand) == 1: return None, None # we can't use this tile for our chosen strategy if not self.is_tile_suitable(tile): return None, None discarded_tile = tile // 4 closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile]) combinations = [] first_index = 0 second_index = 0 if is_man(discarded_tile): first_index = 0 second_index = 8 elif is_pin(discarded_tile): first_index = 9 second_index = 17 elif is_sou(discarded_tile): first_index = 18 second_index = 26 if second_index == 0: # honor tiles if closed_hand_34[discarded_tile] == 3: combinations = [[[discarded_tile] * 3]] else: # to avoid not necessary calculations # we can check only tiles around +-2 discarded tile first_limit = discarded_tile - 2 if first_limit < first_index: first_limit = first_index second_limit = discarded_tile + 2 if second_limit > second_index: second_limit = second_index combinations = self.player.ai.hand_divider.find_valid_combinations( closed_hand_34, first_limit, second_limit, True ) if combinations: combinations = combinations[0] possible_melds = [] for best_meld_34 in combinations: # we can call pon from everyone if is_pon(best_meld_34) and discarded_tile in best_meld_34: if best_meld_34 not in possible_melds: possible_melds.append(best_meld_34) # we can call chi only from left player if is_chi(best_meld_34) and is_kamicha_discard and discarded_tile in best_meld_34: if best_meld_34 not in possible_melds: possible_melds.append(best_meld_34) # we can call melds only with allowed tiles validated_melds = [] for meld in possible_melds: if ( self.is_tile_suitable(meld[0] * 4) and self.is_tile_suitable(meld[1] * 4) and self.is_tile_suitable(meld[2] * 4) ): validated_melds.append(meld) possible_melds = validated_melds if not possible_melds: return None, None chosen_meld_dict = self._find_best_meld_to_open(tile, possible_melds, new_tiles, closed_hand, tile) # we didn't find a good discard candidate after open meld if not chosen_meld_dict: return None, None selected_tile = chosen_meld_dict["discard_tile"] meld = chosen_meld_dict["meld"] shanten = selected_tile.shanten had_to_be_called = self.meld_had_to_be_called(tile) had_to_be_called = had_to_be_called or selected_tile.had_to_be_discarded # each strategy can use their own value to min shanten number if shanten > self.min_shanten: self.player.logger.debug( log.MELD_DEBUG, "After meld shanten is too high for our strategy. Abort melding.", ) return None, None # sometimes we had to call tile, even if it will not improve our hand # otherwise we can call only with improvements of shanten if not had_to_be_called and shanten >= self.player.ai.shanten: self.player.logger.debug( log.MELD_DEBUG, "Meld is not improving hand shanten. Abort melding.", ) return None, None if not self.validate_meld(chosen_meld_dict): self.player.logger.debug( log.MELD_DEBUG, "Meld is suitable for strategy logic. Abort melding.", ) return None, None if not self.should_push_against_threats(chosen_meld_dict): self.player.logger.debug( log.MELD_DEBUG, "Meld is too dangerous to call. Abort melding.", ) return None, None return meld, selected_tile
def should_call_kan(self, tile, open_kan, from_riichi=False): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :param from_riichi: boolean :return: kan type """ # we can't call kan on the latest tile if self.table.count_of_remaining_tiles <= 1: return None # we don't need to add dora for other players if self.player.ai.in_defence: return None if open_kan: # we don't want to start open our hand from called kan if not self.player.is_open_hand: return None # there is no sense to call open kan when we are not in tempai if not self.player.in_tempai: return None # we have a bad wait, rinshan chance is low if len(self.waiting) < 2: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) melds_34 = copy.copy(self.player.meld_34_tiles) tiles = copy.copy(self.player.tiles) closed_hand_tiles = copy.copy(self.player.closed_hand) new_shanten = 0 previous_shanten = 0 new_waits_count = 0 previous_waits_count = 0 # let's check can we upgrade opened pon to the kan pon_melds = [x for x in self.player.meld_34_tiles if is_pon(x)] has_shouminkan_candidate = False for meld in pon_melds: # tile is equal to our already opened pon if tile_34 in meld: has_shouminkan_candidate = True tiles.append(tile) closed_hand_tiles.append(tile) previous_shanten, previous_waits_count = self._calculate_shanten_for_kan( tiles, closed_hand_tiles, self.player.melds ) tiles_34 = TilesConverter.to_34_array(tiles) tiles_34[tile_34] -= 1 new_waiting, new_shanten = self.hand_builder.calculate_waits( tiles_34, self.player.meld_34_tiles ) new_waits_count = self.hand_builder.count_tiles(new_waiting, tiles_34) if not has_shouminkan_candidate: # we don't have enough tiles in the hand if closed_hand_34[tile_34] != 3: return None if open_kan or from_riichi: # this 4 tiles can only be used in kan, no other options previous_waiting, previous_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34) previous_waits_count = self.hand_builder.count_tiles(previous_waiting, closed_hand_34) else: tiles.append(tile) closed_hand_tiles.append(tile) previous_shanten, previous_waits_count = self._calculate_shanten_for_kan( tiles, closed_hand_tiles, self.player.melds ) # shanten calculator doesn't like working with kans, so we pretend it's a pon melds_34 += [[tile_34, tile_34, tile_34]] new_waiting, new_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34) closed_hand_34[tile_34] = 4 new_waits_count = self.hand_builder.count_tiles(new_waiting, closed_hand_34) # it is possible that we don't have results here # when we are in agari state (but without yaku) if previous_shanten is None: return None # it is not possible to reduce number of shanten by calling a kan assert new_shanten >= previous_shanten # if shanten number is the same, we should only call kan if ukeire didn't become worse if new_shanten == previous_shanten: # we cannot improve ukeire by calling kan (not considering the tile we drew from the dead wall) assert new_waits_count <= previous_waits_count if new_waits_count == previous_waits_count: return has_shouminkan_candidate and Meld.CHANKAN or Meld.KAN return None