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_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 is_sanshoku(self, hand): """ The same chi in three suits :param hand: list of hand's sets :return: true|false """ chi_sets = [i for i in hand if is_chi(i)] if len(chi_sets) < 3: return False sou_chi = [] pin_chi = [] man_chi = [] for item in chi_sets: if is_sou(item[0]): sou_chi.append(item) elif is_pin(item[0]): pin_chi.append(item) elif is_man(item[0]): man_chi.append(item) for sou_item in sou_chi: for pin_item in pin_chi: for man_item in man_chi: # cast tile indices to 0..8 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_junchan(self, hand): """ Every set must have at least one terminal, and the pair must be of a terminal tile. Must contain at least one sequence (123 or 789). :param hand: list of hand's sets :return: true|false """ def tile_in_indices(item_set, indices_array): for x in item_set: if x in indices_array: return True return False terminal_sets = 0 count_of_chi = 0 for item in hand: if is_chi(item): count_of_chi += 1 if tile_in_indices(item, TERMINAL_INDICES): terminal_sets += 1 if count_of_chi == 0: return False return terminal_sets == 5
def is_condition_met(self, hand, *args): chi_sets = [i for i in hand if is_chi(i)] if len(chi_sets) < 3: return False sou_chi = [] pin_chi = [] man_chi = [] for item in chi_sets: if is_sou(item[0]): sou_chi.append(item) elif is_pin(item[0]): pin_chi.append(item) elif is_man(item[0]): man_chi.append(item) for sou_item in sou_chi: for pin_item in pin_chi: for man_item in man_chi: # cast tile indices to 0..8 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): chi_sets = [i for i in hand if is_chi(i)] if len(chi_sets) < 3: return False sou_chi = [] pin_chi = [] man_chi = [] for item in chi_sets: if is_sou(item[0]): sou_chi.append(item) elif is_pin(item[0]): pin_chi.append(item) elif is_man(item[0]): man_chi.append(item) sets = [sou_chi, pin_chi, man_chi] for suit_item in sets: if len(suit_item) < 3: continue casted_sets = [] for set_item in suit_item: # cast tiles indices to 0..8 representation casted_sets.append([simplify(set_item[0]), simplify(set_item[1]), simplify(set_item[2])]) if [0, 1, 2] in casted_sets and [3, 4, 5] in casted_sets and [6, 7, 8] in casted_sets: return True return False
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): chi_sets = [i for i in hand if is_chi(i)] count_of_identical_chi = [] for x in chi_sets: count = 0 for y in chi_sets: if x == y: count += 1 count_of_identical_chi.append(count) return len([x for x in count_of_identical_chi if x >= 2]) == 4
def is_condition_met(self, hand, *args): chi_sets = [i for i in hand if is_chi(i)] count_of_identical_chi = 0 for x in chi_sets: count = 0 for y in chi_sets: if x == y: count += 1 if count > count_of_identical_chi: count_of_identical_chi = count return count_of_identical_chi >= 2
def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand, discarded_tile): discarded_tile_34 = discarded_tile // 4 final_results = [] for meld_34 in possible_melds: meld_34_copy = meld_34.copy() closed_hand_copy = closed_hand.copy() meld_type = is_chi(meld_34_copy) and Meld.CHI or Meld.PON meld_34_copy.remove(discarded_tile_34) first_tile = TilesConverter.find_34_tile_in_136_array( meld_34_copy[0], closed_hand_copy) closed_hand_copy.remove(first_tile) second_tile = TilesConverter.find_34_tile_in_136_array( meld_34_copy[1], closed_hand_copy) closed_hand_copy.remove(second_tile) tiles = [first_tile, second_tile, discarded_tile] meld = Meld() meld.type = meld_type meld.tiles = sorted(tiles) melds = self.player.melds + [meld] selected_tile = self.player.ai.hand_builder.choose_tile_to_discard( new_tiles, closed_hand_copy, melds, print_log=False) final_results.append({ 'discard_tile': selected_tile, 'meld_print': TilesConverter.to_one_line_string( [meld_34[0] * 4, meld_34[1] * 4, meld_34[2] * 4]), 'meld': meld }) final_results = sorted(final_results, key=lambda x: (x['discard_tile'].shanten, -x['discard_tile']. ukeire, x['discard_tile'].valuation)) DecisionsLogger.debug(log.MELD_PREPARE, 'Options with meld calling', context=final_results) return final_results[0]
def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand, discarded_tile): discarded_tile_34 = discarded_tile // 4 final_results = [] for meld_34 in possible_melds: meld_34_copy = meld_34.copy() closed_hand_copy = closed_hand.copy() meld_type = is_chi(meld_34_copy) and Meld.CHI or Meld.PON meld_34_copy.remove(discarded_tile_34) first_tile = TilesConverter.find_34_tile_in_136_array(meld_34_copy[0], closed_hand_copy) closed_hand_copy.remove(first_tile) second_tile = TilesConverter.find_34_tile_in_136_array(meld_34_copy[1], closed_hand_copy) closed_hand_copy.remove(second_tile) tiles = [ first_tile, second_tile, discarded_tile ] meld = Meld() meld.type = meld_type meld.tiles = sorted(tiles) melds = self.player.melds + [meld] selected_tile = self.player.ai.hand_builder.choose_tile_to_discard( new_tiles, closed_hand_copy, melds, print_log=False ) final_results.append({ 'discard_tile': selected_tile, 'meld_print': TilesConverter.to_one_line_string([meld_34[0] * 4, meld_34[1] * 4, meld_34[2] * 4]), 'meld': meld }) final_results = sorted(final_results, key=lambda x: (x['discard_tile'].shanten, -x['discard_tile'].ukeire, x['discard_tile'].valuation)) DecisionsLogger.debug(log.MELD_PREPARE, 'Options with meld calling', context=final_results) return final_results[0]
def is_ryanpeiko(self, hand): """ The hand contains two different Iipeikou’s :param hand: list of hand's sets :return: true|false """ chi_sets = [i for i in hand if is_chi(i)] count_of_identical_chi = [] for x in chi_sets: count = 0 for y in chi_sets: if x == y: count += 1 count_of_identical_chi.append(count) return len([x for x in count_of_identical_chi if x >= 2]) == 4
def is_iipeiko(self, hand): """ Hand with two identical chi :param hand: list of hand's sets :return: true|false """ chi_sets = [i for i in hand if is_chi(i)] count_of_identical_chi = 0 for x in chi_sets: count = 0 for y in chi_sets: if x == y: count += 1 if count > count_of_identical_chi: count_of_identical_chi = count return count_of_identical_chi >= 2
def is_ittsu(self, hand): """ Three sets of same suit: 1-2-3, 4-5-6, 7-8-9 :param hand: list of hand's sets :return: true|false """ chi_sets = [i for i in hand if is_chi(i)] if len(chi_sets) < 3: return False sou_chi = [] pin_chi = [] man_chi = [] for item in chi_sets: if is_sou(item[0]): sou_chi.append(item) elif is_pin(item[0]): pin_chi.append(item) elif is_man(item[0]): man_chi.append(item) sets = [sou_chi, pin_chi, man_chi] for suit_item in sets: if len(suit_item) < 3: continue casted_sets = [] for set_item in suit_item: # cast tiles indices to 0..8 representation casted_sets.append([ simplify(set_item[0]), simplify(set_item[1]), simplify(set_item[2]) ]) if [0, 1, 2] in casted_sets and [3, 4, 5] in casted_sets and [ 6, 7, 8 ] in casted_sets: return True return False
def is_condition_met(self, hand, *args): def tile_in_indices(item_set, indices_array): for x in item_set: if x in indices_array: return True return False terminal_sets = 0 count_of_chi = 0 for item in hand: if is_chi(item): count_of_chi += 1 if tile_in_indices(item, TERMINAL_INDICES): terminal_sets += 1 if count_of_chi == 0: return False return terminal_sets == 5
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 boxes_is_chi(boxes): items = sorted([t[0] for t in boxes[0:3]]) if is_honor(items[0]) or is_honor(items[2]): return False return is_chi(items)
def _should_call_riichi_one_sided(self): count_tiles = self.player.ai.hand_builder.count_tiles( self.player.ai.waiting, TilesConverter.to_34_array(self.player.closed_hand) ) waiting = self.player.ai.waiting[0] hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False) tiles = self.player.closed_hand.copy() 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 no the best if count_tiles == 1: 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 # 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 # 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 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 if is_chi(hand_set): # let's only riichi this bad wait if # it has all 4 tiles available or it # it's not too early if 4 <= simplified_waiting <= 6: return count_tiles == 4 or self.player.round_step >= 6 return True
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 calculate_additional_fu(self, win_tile, hand, is_tsumo, player_wind, round_wind, open_sets, called_kan_indices): """ :param win_tile: "136 format" tile :param hand: list of hand's sets :param player_wind: :param round_wind: :param open_sets: array of array with 34 tiles format :param called_kan_indices: array of 34 tiles format :return: int """ win_tile //= 4 additional_fu = 0 closed_hand = [] for set_item in hand: if not is_pair(set_item) and set_item not in open_sets: closed_hand.append(set_item) pon_sets = [x for x in hand if is_pon(x)] chi_sets = [x for x in hand if (win_tile in x and is_chi(x))] closed_hand_indices = closed_hand and reduce(lambda z, y: z + y, closed_hand) or [] # there is no sense to check identical sets unique_chi_sets = [] for item in chi_sets: if item not in unique_chi_sets: unique_chi_sets.append(item) chi_fu_sets = [] for set_item in unique_chi_sets: count_of_open_sets = len([x for x in open_sets if x == set_item]) count_of_sets = len([x for x in chi_sets if x == set_item]) if count_of_open_sets == count_of_sets: continue # penchan waiting if any(x in set_item for x in TERMINAL_INDICES): tile_number = simplify(win_tile) # 1-2-... if set_item.index(win_tile) == 2 and tile_number == 2: chi_fu_sets.append(set_item) # ...-8-9 elif set_item.index(win_tile) == 0 and tile_number == 6: chi_fu_sets.append(set_item) # kanchan waiting 5-...-7 if set_item.index(win_tile) == 1: chi_fu_sets.append(set_item) for set_item in pon_sets: set_was_open = set_item in open_sets is_kan = set_item[0] in called_kan_indices is_honor = set_item[0] in TERMINAL_INDICES + HONOR_INDICES # we win on the third pon tile, our pon will be count as open if not is_tsumo and win_tile in set_item: # 111123 form is exception if len([x for x in closed_hand_indices if x == win_tile]) != 4: set_was_open = True if is_honor: if is_kan: additional_fu += set_was_open and 16 or 32 else: additional_fu += set_was_open and 4 or 8 else: if is_kan: additional_fu += set_was_open and 8 or 16 else: additional_fu += set_was_open and 2 or 4 # valued pair pair = [x for x in hand if is_pair(x)][0][0] valued_indices = [HAKU, HATSU, CHUN, player_wind, round_wind] count_of_valued_pairs = [x for x in valued_indices if x == pair] if len(count_of_valued_pairs): # we can have 4 fu for east-east pair additional_fu += 2 * len(count_of_valued_pairs) pair_was_counted = False if len(chi_fu_sets) and len(unique_chi_sets) == len(chi_fu_sets): if len(chi_fu_sets ) == 1 and pair in chi_fu_sets[0] and win_tile == pair: additional_fu += 2 pair_was_counted = True else: additional_fu += 2 pair_was_counted = True elif additional_fu != 0 and len(chi_fu_sets): # Hand like 123345 # we can't count pinfu yaku here, so let's add additional fu for 123 waiting pair_was_counted = True additional_fu += 2 # separate pair waiting if pair == win_tile: if not len(chi_sets): additional_fu += 2 elif additional_fu != 0 and not pair_was_counted: # we can't count pinfu yaku here, so let's add additional fu additional_fu += 2 return additional_fu
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 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 find_valid_combinations(self, tiles_34, first_index, second_index, hand_not_completed=False): """ Find and return all valid set combinations in given suit :param tiles_34: :param first_index: :param second_index: :param hand_not_completed: in that mode we can return just possible shi or pon sets :return: list of valid combinations """ indices = [] for x in range(first_index, second_index + 1): if tiles_34[x] > 0: indices.extend([x] * tiles_34[x]) if not indices: return [] all_possible_combinations = list(itertools.permutations(indices, 3)) def is_valid_combination(possible_set): if is_chi(possible_set): return True if is_pon(possible_set): return True return False valid_combinations = [] for combination in all_possible_combinations: if is_valid_combination(combination): valid_combinations.append(list(combination)) if not valid_combinations: return [] count_of_needed_combinations = int(len(indices) / 3) # simple case, we have count of sets == count of tiles if ( count_of_needed_combinations == len(valid_combinations) and reduce(lambda z, y: z + y, valid_combinations) == indices ): return [valid_combinations] # filter and remove not possible pon sets for item in valid_combinations: if is_pon(item): count_of_sets = 1 count_of_tiles = 0 while count_of_sets > count_of_tiles: count_of_tiles = len([x for x in indices if x == item[0]]) / 3 count_of_sets = len( [x for x in valid_combinations if x[0] == item[0] and x[1] == item[1] and x[2] == item[2]] ) if count_of_sets > count_of_tiles: valid_combinations.remove(item) # filter and remove not possible chi sets for item in valid_combinations: if is_chi(item): count_of_sets = 5 # TODO calculate real count of possible sets count_of_possible_sets = 4 while count_of_sets > count_of_possible_sets: count_of_sets = len( [x for x in valid_combinations if x[0] == item[0] and x[1] == item[1] and x[2] == item[2]] ) if count_of_sets > count_of_possible_sets: valid_combinations.remove(item) # lit of chi\pon sets for not completed hand if hand_not_completed: return [valid_combinations] # hard case - we can build a lot of sets from our tiles # for example we have 123456 tiles and we can build sets: # [1, 2, 3] [4, 5, 6] [2, 3, 4] [3, 4, 5] # and only two of them valid in the same time [1, 2, 3] [4, 5, 6] possible_combinations = set( itertools.permutations(range(0, len(valid_combinations)), count_of_needed_combinations) ) combinations_results = [] for combination in possible_combinations: result = [] for item in combination: result += valid_combinations[item] result = sorted(result) if result == indices: results = [] for item in combination: results.append(valid_combinations[item]) results = sorted(results, key=lambda z: z[0]) if results not in combinations_results: combinations_results.append(results) return combinations_results
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 _find_best_meld_to_open(self, call_tile_136, possible_melds, new_tiles, closed_hand, discarded_tile): all_tiles_are_suitable = True for tile_136 in closed_hand: all_tiles_are_suitable &= self.is_tile_suitable(tile_136) final_results = [] for meld_34 in possible_melds: # in order to fully emulate the possible hand with meld, we save original melds state, # modify player's melds and then restore original melds state after everything is done melds_original = self.player.melds[:] tiles_original = self.player.tiles[:] tiles = self._find_meld_tiles(closed_hand, meld_34, discarded_tile) meld = MeldPrint() meld.type = is_chi(meld_34) and MeldPrint.CHI or MeldPrint.PON meld.tiles = sorted(tiles) self.player.logger.debug( log.MELD_HAND, f"Hand: {self._format_hand_for_print(closed_hand, discarded_tile, self.player.melds)}" ) # update player hand state to emulate new situation and choose what to discard self.player.tiles = new_tiles[:] self.player.add_called_meld(meld) selected_tile = self.player.ai.hand_builder.choose_tile_to_discard(after_meld=True) # restore original tiles and melds state self.player.tiles = tiles_original self.player.melds = melds_original # we can't find a good discard candidate, so let's skip this if not selected_tile: self.player.logger.debug(log.MELD_DEBUG, "Can't find discard candidate after meld. Abort melding.") continue if not all_tiles_are_suitable and self.is_tile_suitable(selected_tile.tile_to_discard_136): self.player.logger.debug( log.MELD_DEBUG, "We have tiles in our hand that are not suitable to current strategy, " "but we are going to discard tile that we need. Abort melding.", ) continue call_tile_34 = call_tile_136 // 4 # we can't discard the same tile that we called if selected_tile.tile_to_discard_34 == call_tile_34: self.player.logger.debug( log.MELD_DEBUG, "We can't discard same tile that we used for meld. Abort melding." ) continue # we can't discard tile from the other end of the same ryanmen that we called if not is_honor(selected_tile.tile_to_discard_34) and meld.type == MeldPrint.CHI: if is_sou(selected_tile.tile_to_discard_34) and is_sou(call_tile_34): same_suit = True elif is_man(selected_tile.tile_to_discard_34) and is_man(call_tile_34): same_suit = True elif is_pin(selected_tile.tile_to_discard_34) and is_pin(call_tile_34): same_suit = True else: same_suit = False if same_suit: simplified_meld_0 = simplify(meld.tiles[0] // 4) simplified_meld_1 = simplify(meld.tiles[1] // 4) simplified_call = simplify(call_tile_34) simplified_discard = simplify(selected_tile.tile_to_discard_34) kuikae = False if simplified_discard == simplified_call - 3: kuikae_set = [simplified_call - 1, simplified_call - 2] if simplified_meld_0 in kuikae_set and simplified_meld_1 in kuikae_set: kuikae = True elif simplified_discard == simplified_call + 3: kuikae_set = [simplified_call + 1, simplified_call + 2] if simplified_meld_0 in kuikae_set and simplified_meld_1 in kuikae_set: kuikae = True if kuikae: tile_str = TilesConverter.to_one_line_string( [selected_tile.tile_to_discard_136], print_aka_dora=self.player.table.has_aka_dora ) self.player.logger.debug( log.MELD_DEBUG, f"Kuikae discard {tile_str} candidate. Abort melding.", ) continue final_results.append( { "discard_tile": selected_tile, "meld_print": TilesConverter.to_one_line_string([meld_34[0] * 4, meld_34[1] * 4, meld_34[2] * 4]), "meld": meld, } ) if not final_results: self.player.logger.debug(log.MELD_DEBUG, "There are no good discards after melding.") return None final_results = sorted( final_results, key=lambda x: (x["discard_tile"].shanten, -x["discard_tile"].ukeire, x["discard_tile"].valuation), ) self.player.logger.debug( log.MELD_PREPARE, "Tiles could be used for open meld", context=final_results, ) return final_results[0]
def try_to_call_meld(self, tile, is_kamicha_discard): """ When bot can open hand with a set (chi or pon/kan) this method will be called :param tile: 136 format tile :param is_kamicha_discard: boolean :return: Meld and DiscardOption objects or None, None """ # can't call if in riichi if self.player.in_riichi: return None, None closed_hand = self.player.closed_hand[:] # check for appropriate hand size, seems to solve a bug if len(closed_hand) == 1: return None, None # get old shanten value old_tiles_34 = TilesConverter.to_34_array(self.player.tiles) old_shanten = self.shanten.calculate_shanten(old_tiles_34, self.player.open_hand_34_tiles) # setup discarded_tile = tile // 4 new_closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile]) # We will use hand_divider to find possible melds involving the discarded tile. # Check its suit and number to narrow the search conditions # skipping this will break the default mahjong functions combinations = [] first_index = 0 second_index = 0 if is_man(discarded_tile): first_index = 0 second_index = 8 elif is_pin(discarded_tile): first_index = 9 second_index = 17 elif is_sou(discarded_tile): first_index = 18 second_index = 26 if second_index == 0: # honor tiles if new_closed_hand_34[discarded_tile] == 3: combinations = [[[discarded_tile] * 3]] else: # to avoid not necessary calculations # we can check only tiles around +-2 discarded tile first_limit = discarded_tile - 2 if first_limit < first_index: first_limit = first_index second_limit = discarded_tile + 2 if second_limit > second_index: second_limit = second_index combinations = self.hand_divider.find_valid_combinations(new_closed_hand_34, first_limit, second_limit, True) # Reduce combinations to list of melds if combinations: combinations = combinations[0] # Verify that a meld can be called possible_melds = [] for meld_34 in combinations: # we can call pon from everyone if is_pon(meld_34) and discarded_tile in meld_34: if meld_34 not in possible_melds: possible_melds.append(meld_34) # we can call chi only from left player if is_chi(meld_34) and is_kamicha_discard and discarded_tile in meld_34: if meld_34 not in possible_melds: possible_melds.append(meld_34) # For each possible meld, check if calling it and discarding can improve shanten new_shanten = float('inf') discard_136 = None tiles = None for meld_34 in possible_melds: shanten, disc = self.meldDiscard(meld_34, tile) if shanten < new_shanten: new_shanten, discard_136 = shanten, disc tiles = meld_34 # If shanten can be improved by calling meld, call it if new_shanten < old_shanten: meld = Meld() meld.type = is_chi(tiles) and Meld.CHI or Meld.PON # convert meld tiles back to 136 format for Meld type return # find them in a copy of the closed hand and remove tiles.remove(discarded_tile) first_tile = TilesConverter.find_34_tile_in_136_array(tiles[0], closed_hand) closed_hand.remove(first_tile) second_tile = TilesConverter.find_34_tile_in_136_array(tiles[1], closed_hand) closed_hand.remove(second_tile) tiles_136 = [ first_tile, second_tile, tile ] discard_136 = TilesConverter.find_34_tile_in_136_array(discard_136 // 4, closed_hand) meld.tiles = sorted(tiles_136) return meld, discard_136 return None, None
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 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()