def should_call_riichi(self): # empty waiting can be found in some cases if not self.waiting: return False if self.in_defence: return False # we have a good wait, let's riichi if len(self.waiting) > 1: return True waiting = self.waiting[0] tiles = self.player.closed_hand + [waiting * 4] closed_melds = [x for x in self.player.melds if not x.opened] for meld in closed_melds: tiles.extend(meld.tiles[:3]) tiles_34 = TilesConverter.to_34_array(tiles) results = self.hand_divider.divide_hand(tiles_34) result = results[0] count_of_pairs = len([x for x in result if is_pair(x)]) # with chitoitsu we can call a riichi with pair wait if count_of_pairs == 7: return True for hand_set in result: # better to not call a riichi for a pair wait # it can be easily improved if is_pair(hand_set) and waiting in hand_set: return False return True
def 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_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_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_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 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 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_or_kan(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": 4, "reason": FuCalculator.DOUBLE_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_set = (open_meld and (open_meld.type == Meld.KAN or open_meld.type == Meld.SHOUMINKAN)) 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_set: 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_set: 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.options.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.options.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 _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 _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 _choose_best_discard_in_tempai(self, discard_options, after_meld): discard_desc = [] closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) for discard_option in discard_options: call_riichi = discard_option.with_riichi tiles_original, discard_original = self.emulate_discard( discard_option) is_furiten = self._is_discard_option_furiten(discard_option) if len(discard_option.waiting) == 1: waiting = discard_option.waiting[0] cost_x_ukeire, hand_cost = self._estimate_cost_x_ukeire( discard_option, call_riichi) # let's check if this is a tanki wait results, tiles_34 = self.divide_hand(self.player.tiles, waiting) result = results[0] tanki_type = None is_tanki = False for hand_set in result: if waiting not in hand_set: continue if is_pair(hand_set): is_tanki = True if is_honor(waiting): # TODO: differentiate between self honor and honor for all players if waiting in self.player.valued_honors: tanki_type = TankiWait.TANKI_WAIT_ALL_YAKUHAI else: tanki_type = TankiWait.TANKI_WAIT_NON_YAKUHAI break simplified_waiting = simplify(waiting) have_suji, have_kabe = self.check_suji_and_kabe( closed_tiles_34, waiting) # TODO: not sure about suji/kabe priority, so we keep them same for now if 3 <= simplified_waiting <= 5: if have_suji or have_kabe: tanki_type = TankiWait.TANKI_WAIT_456_KABE else: tanki_type = TankiWait.TANKI_WAIT_456_RAW elif 2 <= simplified_waiting <= 6: if have_suji or have_kabe: tanki_type = TankiWait.TANKI_WAIT_37_KABE else: tanki_type = TankiWait.TANKI_WAIT_37_RAW elif 1 <= simplified_waiting <= 7: if have_suji or have_kabe: tanki_type = TankiWait.TANKI_WAIT_28_KABE else: tanki_type = TankiWait.TANKI_WAIT_28_RAW else: if have_suji or have_kabe: tanki_type = TankiWait.TANKI_WAIT_69_KABE else: tanki_type = TankiWait.TANKI_WAIT_69_RAW break tempai_descriptor = { "discard_option": discard_option, "hand_cost": hand_cost, "cost_x_ukeire": cost_x_ukeire, "is_furiten": is_furiten, "is_tanki": is_tanki, "tanki_type": tanki_type, "max_danger": discard_option.danger.get_max_danger(), "sum_danger": discard_option.danger.get_sum_danger(), "weighted_danger": discard_option.danger.get_weighted_danger(), } discard_desc.append(tempai_descriptor) else: cost_x_ukeire, _ = self._estimate_cost_x_ukeire( discard_option, call_riichi) tempai_descriptor = { "discard_option": discard_option, "hand_cost": None, "cost_x_ukeire": cost_x_ukeire, "is_furiten": is_furiten, "is_tanki": False, "tanki_type": None, "max_danger": discard_option.danger.get_max_danger(), "sum_danger": discard_option.danger.get_sum_danger(), "weighted_danger": discard_option.danger.get_weighted_danger(), } discard_desc.append(tempai_descriptor) # save descriptor to discard option for future users discard_option.tempai_descriptor = tempai_descriptor # reverse all temporary tile tweaks self.restore_after_emulate_discard(tiles_original, discard_original) discard_desc = sorted( discard_desc, key=lambda k: (-k["cost_x_ukeire"], k["is_furiten"], k["weighted_danger"])) # if we don't have any good options, e.g. all our possible waits are karaten if discard_desc[0]["cost_x_ukeire"] == 0: # we still choose between options that give us tempai, because we may be going to formal tempai # with no hand cost return self._choose_safest_tile(discard_options) num_tanki_waits = len([x for x in discard_desc if x["is_tanki"]]) # what if all our waits are tanki waits? we need a special handling for that case if num_tanki_waits == len(discard_options): return self._choose_best_tanki_wait(discard_desc) best_discard_desc = [ x for x in discard_desc if x["cost_x_ukeire"] == discard_desc[0]["cost_x_ukeire"] ] best_discard_desc = sorted(best_discard_desc, key=lambda k: (k["is_furiten"], k["weighted_danger"])) # if we have several options that give us similar wait return best_discard_desc[0]["discard_option"]
def _choose_best_discard_in_tempai(self, tiles, melds, discard_options): # first of all we find tiles that have the best hand cost * ukeire value call_riichi = not self.player.is_open_hand discard_desc = [] player_tiles_copy = self.player.tiles.copy() player_melds_copy = self.player.melds.copy() closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) for discard_option in discard_options: tile = discard_option.find_tile_in_hand(self.player.closed_hand) # temporary remove discard option to estimate hand value self.player.tiles = tiles.copy() self.player.tiles.remove(tile) # temporary replace melds self.player.melds = melds.copy() # for kabe/suji handling discarded_tile = Tile(tile, False) self.player.discards.append(discarded_tile) is_furiten = self._is_discard_option_furiten(discard_option) if len(discard_option.waiting) == 1: waiting = discard_option.waiting[0] cost_x_ukeire, hand_cost = self._estimate_cost_x_ukeire(discard_option, call_riichi) # let's check if this is a tanki wait results, tiles_34 = self.divide_hand(self.player.tiles, waiting) result = results[0] tanki_type = None is_tanki = False for hand_set in result: if waiting not in hand_set: continue if is_pair(hand_set): is_tanki = True if is_honor(waiting): # TODO: differentiate between self honor and honor for all players if waiting in self.player.valued_honors: tanki_type = self.TankiWait.TANKI_WAIT_ALL_YAKUHAI else: tanki_type = self.TankiWait.TANKI_WAIT_NON_YAKUHAI break simplified_waiting = simplify(waiting) have_suji, have_kabe = self.check_suji_and_kabe(closed_tiles_34, waiting) # TODO: not sure about suji/kabe priority, so we keep them same for now if 3 <= simplified_waiting <= 5: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_456_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_456_RAW elif 2 <= simplified_waiting <= 6: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_37_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_37_RAW elif 1 <= simplified_waiting <= 7: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_28_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_28_RAW else: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_69_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_69_RAW break discard_desc.append({ 'discard_option': discard_option, 'hand_cost': hand_cost, 'cost_x_ukeire': cost_x_ukeire, 'is_furiten': is_furiten, 'is_tanki': is_tanki, 'tanki_type': tanki_type }) else: cost_x_ukeire, _ = self._estimate_cost_x_ukeire(discard_option, call_riichi) discard_desc.append({ 'discard_option': discard_option, 'hand_cost': None, 'cost_x_ukeire': cost_x_ukeire, 'is_furiten': is_furiten, 'is_tanki': False, 'tanki_type': None }) # reverse all temporary tile tweaks self.player.tiles = player_tiles_copy self.player.melds = player_melds_copy self.player.discards.remove(discarded_tile) discard_desc = sorted(discard_desc, key=lambda k: (k['cost_x_ukeire'], not k['is_furiten']), reverse=True) # if we don't have any good options, e.g. all our possible waits ara karaten # FIXME: in that case, discard the safest tile if discard_desc[0]['cost_x_ukeire'] == 0: return sorted(discard_options, key=lambda x: x.valuation)[0] num_tanki_waits = len([x for x in discard_desc if x['is_tanki']]) # what if all our waits are tanki waits? we need a special handling for that case if num_tanki_waits == len(discard_options): return self._choose_best_tanki_wait(discard_desc) best_discard_desc = [x for x in discard_desc if x['cost_x_ukeire'] == discard_desc[0]['cost_x_ukeire']] # we only have one best option based on ukeire and cost, nothing more to do here if len(best_discard_desc) == 1: return best_discard_desc[0]['discard_option'] # if we have several options that give us similar wait # FIXME: 1. we find the safest tile to discard # FIXME: 2. if safeness is the same, we try to discard non-dora tiles return best_discard_desc[0]['discard_option']
def should_call_riichi(self): logger.info("Can call a reach!") # empty waiting can be found in some cases if not self.waiting: logger.info("However it is impossible to win.") return False # In pushing state, it's better to call it if self.pushing: logger.info("Go for it! The player is in pushing state.") return True # Get the rank EV after round 3 if self.table.round_number >= 5: # DEBUG: set this to 0 try: possible_hand_values = [self.estimate_hand_value(tile, call_riichi=True).cost["main"] for tile in self.waiting] except Exception as e: print(e) possible_hand_values = [2000] hand_value = sum(possible_hand_values) / len(possible_hand_values) hand_value += self.table.count_of_riichi_sticks * 1000 if self.player.is_dealer: hand_value += 700 # EV for dealer combo lose_estimation = 6000 if self.player.is_dealer else 7000 hand_shape = "pro_bad_shape" if self.wanted_tiles_count <= 4 else "pro_good_shape" rank_ev = self.defence.get_rank_ev(hand_value, lose_estimation, COUNTER_RATIO[hand_shape][len(self.player.discards)]) logger.info('''Cowboy: Proactive reach: Hand value: {} Hand shape: {} Is dealer: {} Current ranking: {} '''.format(hand_value, hand_shape, self.player.is_dealer, self.table.get_players_sorted_by_scores())) logger.info("Rank EV for proactive reach: {}".format(rank_ev)) if rank_ev < 0: logger.info("It's better to fold.") return False else: logger.info("Go for it!") return True should_attack = not self.defence.should_go_to_defence_mode() # For bad shape, at least 1 dora is required # Get count of dora dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles]) # aka dora dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_open_tanyao)]) if self.wanted_tiles_count <= 4 and dora_count == 0 and not self.player.is_dealer: should_attack = False logger.info("A bad shape with no dora, don't call it.") # # If player is on the top, no need to call reach # if self.player == self.player.table.get_players_sorted_by_scores()[0] and self.player.scores > 30000: # should_attack = False # logger.info("Player is in 1st position, no need to call reach.") if should_attack: # If we are proactive, let's set the state! logger.info("Go for it!") if self.player.play_state == "PREPARING": # If not changed in defense actions if self.wanted_tiles_count > 4: self.player.set_state("PROACTIVE_GOODSHAPE") else: self.player.set_state("PROACTIVE_BADSHAPE") return True else: logger.info("However it's better to fold.") return False # These codes are unreachable, it is fine. waiting = self.waiting[0] tiles = self.player.closed_hand + [waiting * 4] closed_melds = [x for x in self.player.melds if not x.opened] for meld in closed_melds: tiles.extend(meld.tiles[:3]) tiles_34 = TilesConverter.to_34_array(tiles) results = self.hand_divider.divide_hand(tiles_34) result = results[0] count_of_pairs = len([x for x in result if is_pair(x)]) # with chitoitsu we can call a riichi with pair wait if count_of_pairs == 7: return True for hand_set in result: # better to not call a riichi for a pair wait # it can be easily improved if is_pair(hand_set) and waiting in hand_set: return False return True
def _choose_best_discard_in_tempai(self, tiles, melds, discard_options): # first of all we find tiles that have the best hand cost * ukeire value call_riichi = not self.player.is_open_hand discard_desc = [] player_tiles_copy = self.player.tiles.copy() player_melds_copy = self.player.melds.copy() closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) for discard_option in discard_options: tile = discard_option.find_tile_in_hand(self.player.closed_hand) # temporary remove discard option to estimate hand value self.player.tiles = tiles.copy() self.player.tiles.remove(tile) # temporary replace melds self.player.melds = melds.copy() # for kabe/suji handling discarded_tile = Tile(tile, False) self.player.discards.append(discarded_tile) is_furiten = self._is_discard_option_furiten(discard_option) if len(discard_option.waiting) == 1: waiting = discard_option.waiting[0] cost_x_ukeire, hand_cost = self._estimate_cost_x_ukeire(discard_option, call_riichi) # let's check if this is a tanki wait results, tiles_34 = self.divide_hand(self.player.tiles, waiting) result = results[0] tanki_type = None is_tanki = False for hand_set in result: if waiting not in hand_set: continue if is_pair(hand_set): is_tanki = True if is_honor(waiting): # TODO: differentiate between self honor and honor for all players if waiting in self.player.valued_honors: tanki_type = self.TankiWait.TANKI_WAIT_ALL_YAKUHAI else: tanki_type = self.TankiWait.TANKI_WAIT_NON_YAKUHAI break simplified_waiting = simplify(waiting) have_suji, have_kabe = self.check_suji_and_kabe(closed_tiles_34, waiting) # TODO: not sure about suji/kabe priority, so we keep them same for now if 3 <= simplified_waiting <= 5: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_456_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_456_RAW elif 2 <= simplified_waiting <= 6: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_37_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_37_RAW elif 1 <= simplified_waiting <= 7: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_28_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_28_RAW else: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_69_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_69_RAW break discard_desc.append({ 'discard_option': discard_option, 'hand_cost': hand_cost, 'cost_x_ukeire': cost_x_ukeire, 'is_furiten': is_furiten, 'is_tanki': is_tanki, 'tanki_type': tanki_type }) else: cost_x_ukeire, _ = self._estimate_cost_x_ukeire(discard_option, call_riichi) discard_desc.append({ 'discard_option': discard_option, 'hand_cost': None, 'cost_x_ukeire': cost_x_ukeire, 'is_furiten': is_furiten, 'is_tanki': False, 'tanki_type': None }) # reverse all temporary tile tweaks self.player.tiles = player_tiles_copy self.player.melds = player_melds_copy self.player.discards.remove(discarded_tile) discard_desc = sorted(discard_desc, key=lambda k: (k['cost_x_ukeire'], not k['is_furiten']), reverse=True) # if we don't have any good options, e.g. all our possible waits ara karaten # FIXME: in that case, discard the safest tile if discard_desc[0]['cost_x_ukeire'] == 0: return sorted(discard_options, key=lambda x: x.valuation)[0] num_tanki_waits = len([x for x in discard_desc if x['is_tanki']]) # what if all our waits are tanki waits? we need a special handling for that case if num_tanki_waits == len(discard_options): return self._choose_best_tanki_wait(discard_desc) best_discard_desc = [x for x in discard_desc if x['cost_x_ukeire'] == discard_desc[0]['cost_x_ukeire']] # we only have one best option based on ukeire and cost, nothing more to do here if len(best_discard_desc) == 1: return best_discard_desc[0]['discard_option'] # if we have several options that give us similar wait # FIXME: 1. we find the safest tile to discard # FIXME: 2. if safeness is the same, we try to discard non-dora tiles return best_discard_desc[0]['discard_option']
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