def is_sanshoku_douko(self, hand): """ Three pon sets consisting of the same numbers in all three suits :param hand: list of hand's sets :return: true|false """ pon_sets = [i for i in hand if is_pon(i)] if len(pon_sets) < 3: return False sou_pon = [] pin_pon = [] man_pon = [] for item in pon_sets: if is_sou(item[0]): sou_pon.append(item) elif is_pin(item[0]): pin_pon.append(item) elif is_man(item[0]): man_pon.append(item) for sou_item in sou_pon: for pin_item in pin_pon: for man_item in man_pon: # cast tile indices to 1..9 representation sou_item = [simplify(x) for x in sou_item] pin_item = [simplify(x) for x in pin_item] man_item = [simplify(x) for x in man_item] if sou_item == pin_item == man_item: return True return False
def is_condition_met(self, hand, *args): 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_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_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_condition_met(self, hand, *args): pon_sets = [i for i in hand if is_pon_or_kan(i)] if len(pon_sets) < 3: return False sou_pon = [] pin_pon = [] man_pon = [] for item in pon_sets: if is_sou(item[0]): sou_pon.append(item) elif is_pin(item[0]): pin_pon.append(item) elif is_man(item[0]): man_pon.append(item) for sou_item in sou_pon: for pin_item in pin_pon: for man_item in man_pon: # cast tile indices to 1..9 representation sou_item = set([simplify(x) for x in sou_item]) pin_item = set([simplify(x) for x in pin_item]) man_item = set([simplify(x) for x in man_item]) if sou_item == pin_item == man_item: return True return False
def _is_matagi_suji(self, enemy_analyzer, tile_analyze_34): discards = enemy_analyzer.enemy.discards discards_34 = [x.value // 4 for x in enemy_analyzer.enemy.discards] # too early to check matagi suji if len(discards) <= 5: return False # on middle stage check matagi pattern only for one latest discard elif len(discards) <= 9: latest_discards = [x for x in discards if not x.is_tsumogiri][-1:] else: # on late stage check matagi pattern for two latest discards latest_discards = [x for x in discards if not x.is_tsumogiri][-2:] latest_discards_34 = [x.value // 4 for x in latest_discards] # make sure that these discards are unique latest_discards_34 = list(set(latest_discards_34)) matagi_patterns_config = [ {"tile": 2, "dangers": [[1, 4]]}, {"tile": 3, "dangers": [[1, 4], [2, 5]]}, {"tile": 4, "dangers": [[2, 5], [3, 6]]}, {"tile": 5, "dangers": [[3, 6], [4, 7]]}, {"tile": 6, "dangers": [[4, 7], [5, 8]]}, {"tile": 7, "dangers": [[5, 8], [6, 9]]}, {"tile": 8, "dangers": [[6, 9]]}, ] for enemy_discard_34 in latest_discards_34: if not is_tiles_same_suit(enemy_discard_34, tile_analyze_34): continue # +1 here to make it easier read matagi patterns enemy_discard_simplified = simplify(enemy_discard_34) + 1 tile_analyze_simplified = simplify(tile_analyze_34) + 1 for matagi_pattern_config in matagi_patterns_config: if matagi_pattern_config["tile"] != enemy_discard_simplified: continue same_suit_simple_discards = [] for is_suit in [is_pin, is_sou, is_man]: if not is_suit(tile_analyze_34): continue same_suit_simple_discards = [] for discard_34 in discards_34: if is_suit(discard_34): # +1 here to make it easier to read same_suit_simple_discards.append(simplify(discard_34) + 1) for danger in matagi_pattern_config["dangers"]: has_suji_in_discard = len(list(set(same_suit_simple_discards) & set(danger))) != 0 if not has_suji_in_discard and tile_analyze_simplified in danger: return True return False
def is_aidayonken_pattern(self, enemy_analyzer, tile_analyze_34): discards = enemy_analyzer.enemy_discards_until_all_tsumogiri discards_34 = [x.value // 4 for x in discards] patterns_config = [ { "pattern": [1, 6], "danger": [2, 5], }, { "pattern": [2, 7], "danger": [3, 6], }, { "pattern": [3, 8], "danger": [4, 7], }, { "pattern": [4, 9], "danger": [5, 8], }, ] for is_suit in [is_pin, is_sou, is_man]: if not is_suit(tile_analyze_34): continue same_suit_simple_discards = [] for discard_34 in discards_34: if is_suit(discard_34): # +1 here to make it easier to read same_suit_simple_discards.append(simplify(discard_34) + 1) # +1 here to make it easier to read tile_analyze_simplified = simplify(tile_analyze_34) + 1 for pattern_config in patterns_config: has_pattern = (list( set(same_suit_simple_discards) & set(pattern_config["pattern"])) == pattern_config["pattern"]) if not has_pattern: continue has_suji_in_discard = len( list( set(same_suit_simple_discards) & set(pattern_config["danger"]))) != 0 # we found aidayonken pattern in the discard # and aidayonken danger tiles are not in the discard # in that case we can increase danger for them if not has_suji_in_discard and tile_analyze_simplified in pattern_config[ "danger"]: return True return False
def _process_danger_for_2_8_tiles_suji_and_kabe( self, enemy_analyzer, tile_34, number_of_revealed_tiles, suji_tiles, kabe_tiles ): have_strong_kabe = [x for x in kabe_tiles if tile_34 == x["tile"] and x["type"] == Kabe.STRONG_KABE] if have_strong_kabe: if enemy_analyzer.enemy.is_open_hand: if number_of_revealed_tiles == 1: return TileDanger.SHONPAI_KABE_STRONG_OPEN_HAND else: return TileDanger.NON_SHONPAI_KABE_STRONG_OPEN_HAND else: if number_of_revealed_tiles == 1: return TileDanger.SHONPAI_KABE_STRONG else: return TileDanger.NON_SHONPAI_KABE_STRONG have_weak_kabe = [x for x in kabe_tiles if tile_34 == x["tile"] and x["type"] == Kabe.WEAK_KABE] if have_weak_kabe: if enemy_analyzer.enemy.is_open_hand: if number_of_revealed_tiles == 1: return TileDanger.SHONPAI_KABE_WEAK_OPEN_HAND else: return TileDanger.NON_SHONPAI_KABE_WEAK_OPEN_HAND else: if number_of_revealed_tiles == 1: return TileDanger.SHONPAI_KABE_WEAK else: return TileDanger.NON_SHONPAI_KABE_WEAK # only consider suji if there is no kabe have_suji = [x for x in suji_tiles if tile_34 == x] if have_suji: if enemy_analyzer.enemy.riichi_tile_136 is not None: enemy_riichi_tile_34 = enemy_analyzer.enemy.riichi_tile_136 // 4 riichi_on_suji = [x for x in suji_tiles if enemy_riichi_tile_34 == x] # if it's 2378, then check if riichi was on suji tile if simplify(enemy_riichi_tile_34) == 4 and (simplify(tile_34) == 1 or simplify(tile_34) == 7): return TileDanger.SUJI_28_ON_RIICHI if simplify(tile_34) == 2 or simplify(tile_34) == 6: if 3 <= simplify(enemy_riichi_tile_34) <= 5 and riichi_on_suji: return TileDanger.SUJI_37_ON_RIICHI elif enemy_analyzer.enemy.is_open_hand: return TileDanger.SUJI_OPEN_HAND return TileDanger.SUJI return None
def _count_of_shuntsu(tiles, suit): suit_tiles = [] for x in range(0, 34): tile = tiles[x] if not tile: continue if suit(x): suit_tiles.append(x) count_of_left_tiles = 0 count_of_middle_tiles = 0 count_of_right_tiles = 0 simple_tiles = [simplify(x) for x in suit_tiles] for x in range(0, len(simple_tiles)): tile = simple_tiles[x] if tile + 1 in simple_tiles and tile + 2 in simple_tiles: count_of_left_tiles += 1 if tile - 1 in simple_tiles and tile + 1 in simple_tiles: count_of_middle_tiles += 1 if tile - 2 in simple_tiles and tile - 1 in simple_tiles: count_of_right_tiles += 1 return (count_of_left_tiles + count_of_middle_tiles + count_of_right_tiles) // 3
def _find_ryanmen_waits(tiles, suit): suit_tiles = [] for x in range(0, 34): tile = tiles[x] if not tile: continue if suit(x): suit_tiles.append(x) count_of_ryanmen_waits = 0 simple_tiles = [simplify(x) for x in suit_tiles] for x in range(0, len(simple_tiles)): tile = simple_tiles[x] # we cant build ryanmen with 1 and 9 if tile == 1 or tile == 9: continue # bordered tile if x + 1 == len(simple_tiles): continue if tile + 1 == simple_tiles[x + 1]: count_of_ryanmen_waits += 1 return count_of_ryanmen_waits
def _find_ryanmen_waits(self, tiles, suit): suit_tiles = [] for x in range(0, 34): tile = tiles[x] if not tile: continue if suit(x): suit_tiles.append(x) count_of_ryanmen_waits = 0 simple_tiles = [simplify(x) for x in suit_tiles] for x in range(0, len(simple_tiles)): tile = simple_tiles[x] # we cant build ryanmen with 1 and 9 if tile == 1 or tile == 9: continue # bordered tile if x + 1 == len(simple_tiles): continue if tile + 1 == simple_tiles[x + 1]: count_of_ryanmen_waits += 1 return count_of_ryanmen_waits
def is_condition_met(self, hand, allow_other_sets, *args): sou_sets = 0 pin_sets = 0 man_sets = 0 honor_sets = 0 for item in hand: if is_sou(item[0]): sou_sets += 1 elif is_pin(item[0]): pin_sets += 1 elif is_man(item[0]): man_sets += 1 else: honor_sets += 1 sets = [sou_sets, pin_sets, man_sets] only_one_suit = len([x for x in sets if x != 0]) == 1 if not only_one_suit or honor_sets > 0: return False if not allow_other_sets and pin_sets == 0: # if we are not allowing other sets than pins return False indices = reduce(lambda z, y: z + y, hand) # cast tile indices to 0..8 representation indices = [simplify(x) for x in indices] # check for pairs for x in range(1, 8): if len([y for y in indices if y == x]) != 2: return False return True
def _suits_tiles(self, tiles_34): """ Return tiles separated by suits :param tiles_34: :return: """ suits = [ [0] * 9, [0] * 9, [0] * 9, ] for tile in range(0, EAST): total_tiles = self.player.total_tiles(tile, tiles_34) if not total_tiles: continue suit_index = None simplified_tile = simplify(tile) if is_man(tile): suit_index = 0 if is_pin(tile): suit_index = 1 if is_sou(tile): suit_index = 2 suits[suit_index][simplified_tile] += total_tiles return suits
def find_suji(self, safe_tiles_34): suji = [] suits = [[], [], []] # let's cast each tile to 0-8 presentation for tile in safe_tiles_34: if is_man(tile): suits[0].append(simplify(tile)) if is_pin(tile): suits[1].append(simplify(tile)) if is_sou(tile): suits[2].append(simplify(tile)) for x in range(0, 3): simplified_tiles = suits[x] base = x * 9 # 1-4-7 if 3 in simplified_tiles: suji.append(self.FIRST_SUJI + base) # double 1-4-7 if 0 in simplified_tiles and 6 in simplified_tiles: suji.append(self.FIRST_SUJI + base) # 2-5-8 if 4 in simplified_tiles: suji.append(self.SECOND_SUJI + base) # double 2-5-8 if 1 in simplified_tiles and 7 in simplified_tiles: suji.append(self.SECOND_SUJI + base) # 3-6-9 if 5 in simplified_tiles: suji.append(self.THIRD_SUJI + base) # double 3-6-9 if 2 in simplified_tiles and 8 in simplified_tiles: suji.append(self.THIRD_SUJI + base) suji = list(set(suji)) return suji
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 _simplified_danger_valuation(self, discard_option): tile_34 = discard_option.tile_to_discard_34 tile_136 = discard_option.tile_to_discard_136 number_of_revealed_tiles = self.player.number_of_revealed_tiles( tile_34, TilesConverter.to_34_array(self.player.closed_hand)) if is_honor(tile_34): if not self.player.table.is_common_yakuhai(tile_34): if number_of_revealed_tiles == 4: simple_danger = 0 elif number_of_revealed_tiles == 3: simple_danger = 10 elif number_of_revealed_tiles == 2: simple_danger = 20 else: simple_danger = 30 else: if number_of_revealed_tiles == 4: simple_danger = 0 elif number_of_revealed_tiles == 3: simple_danger = 11 elif number_of_revealed_tiles == 2: simple_danger = 21 else: simple_danger = 32 elif is_terminal(tile_34): simple_danger = 100 elif simplify(tile_34) < 2 or simplify(tile_34) > 6: # 2, 3 or 7, 8 simple_danger = 200 else: # 4, 5, 6 simple_danger = 300 if simple_danger != 0: simple_danger += plus_dora( tile_136, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora) return simple_danger
def is_chuuren_poutou(self, hand): """ The hand contains 1-1-1-2-3-4-5-6-7-8-9-9-9 of one suit, plus any other tile of the same suit. :param hand: list of hand's sets :return: true|false """ sou_sets = 0 pin_sets = 0 man_sets = 0 honor_sets = 0 for item in hand: if is_sou(item[0]): sou_sets += 1 elif is_pin(item[0]): pin_sets += 1 elif is_man(item[0]): man_sets += 1 else: honor_sets += 1 sets = [sou_sets, pin_sets, man_sets] only_one_suit = len([x for x in sets if x != 0]) == 1 if not only_one_suit or honor_sets > 0: return False indices = reduce(lambda z, y: z + y, hand) # cast tile indices to 0..8 representation indices = [simplify(x) for x in indices] # 1-1-1 if len([x for x in indices if x == 0]) < 3: return False # 9-9-9 if len([x for x in indices if x == 8]) < 3: return False # 1-2-3-4-5-6-7-8-9 and one tile to any of them indices.remove(0) indices.remove(0) indices.remove(8) indices.remove(8) for x in range(0, 9): if x in indices: indices.remove(x) if len(indices) == 1: return True return False
def calculate_value(self, shanten=None): # base is 100 for ability to mark tiles as not needed (like set value to 50) value = 100 honored_value = 20 # we don't need to keep honor tiles in almost completed hand if shanten and shanten <= 2: honored_value = 0 if is_honor(self.tile_to_discard): if self.tile_to_discard in self.player.valued_honors: count_of_winds = [ x for x in self.player.valued_honors if x == self.tile_to_discard ] # for west-west, east-east we had to double tile value value += honored_value * len(count_of_winds) else: # suits suit_tile_grades = [10, 20, 30, 40, 50, 40, 30, 20, 10] simplified_tile = simplify(self.tile_to_discard) value += suit_tile_grades[simplified_tile] count_of_dora = plus_dora(self.tile_to_discard * 4, self.player.table.dora_indicators) if is_aka_dora(self.tile_to_discard * 4, self.player.table.has_open_tanyao): count_of_dora += 1 value += 50 * count_of_dora if is_honor(self.tile_to_discard): # depends on how much honor tiles were discarded # we will decrease tile value discard_percentage = [100, 75, 20, 0, 0] discarded_tiles = self.player.table.revealed_tiles[ self.tile_to_discard] value = (value * discard_percentage[discarded_tiles]) / 100 # three honor tiles were discarded, # so we don't need this tile anymore if value == 0: self.had_to_be_discarded = True self.valuation = value
def is_condition_met(self, hand, *args): sou_sets = 0 pin_sets = 0 man_sets = 0 honor_sets = 0 for item in hand: if is_sou(item[0]): sou_sets += 1 elif is_pin(item[0]): pin_sets += 1 elif is_man(item[0]): man_sets += 1 else: honor_sets += 1 sets = [sou_sets, pin_sets, man_sets] only_one_suit = len([x for x in sets if x != 0]) == 1 if not only_one_suit or honor_sets > 0: return False indices = reduce(lambda z, y: z + y, hand) # cast tile indices to 0..8 representation indices = [simplify(x) for x in indices] # 1-1-1 if len([x for x in indices if x == 0]) < 3: return False # 9-9-9 if len([x for x in indices if x == 8]) < 3: return False # 1-2-3-4-5-6-7-8-9 and one tile to any of them indices.remove(0) indices.remove(0) indices.remove(8) indices.remove(8) for x in range(0, 9): if x in indices: indices.remove(x) if len(indices) == 1: return True return False
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 find_tiles_to_discard(self, players): found_suji = [] for player in players: suji = [] suits = [[], [], []] # let's cast each tile to 0-8 presentation safe_tiles = player.all_safe_tiles for tile in safe_tiles: if is_man(tile): suits[0].append(simplify(tile)) if is_pin(tile): suits[1].append(simplify(tile)) if is_sou(tile): suits[2].append(simplify(tile)) for x in range(0, 3): simplified_tiles = suits[x] base = x * 9 # 1-4-7 if 3 in simplified_tiles: suji.append(self.FIRST_SUJI + base) # double 1-4-7 if 0 in simplified_tiles and 6 in simplified_tiles: suji.append(self.FIRST_SUJI + base) # 2-5-8 if 4 in simplified_tiles: suji.append(self.SECOND_SUJI + base) # double 2-5-8 if 1 in simplified_tiles and 7 in simplified_tiles: suji.append(self.SECOND_SUJI + base) # 3-6-9 if 5 in simplified_tiles: suji.append(self.THIRD_SUJI + base) # double 3-6-9 if 2 in simplified_tiles and 8 in simplified_tiles: suji.append(self.THIRD_SUJI + base) suji = list(set(suji)) found_suji.append(suji) if not found_suji: return [] common_suji = list(set.intersection(*map(set, found_suji))) tiles = [] for suji in common_suji: if not suji: continue tiles.extend(self._suji_tiles(suji)) return tiles
def _get_early_danger_bonus(self, enemy_analyzer, tile_analyze_34, has_other_danger_bonus): discards = enemy_analyzer.enemy_discards_until_all_tsumogiri discards_34 = [x.value // 4 for x in discards] assert not is_honor(tile_analyze_34) # +1 here to make it easier to read tile_analyze_simplified = simplify(tile_analyze_34) + 1 # we only those border tiles if tile_analyze_simplified not in [1, 2, 8, 9]: return None # too early to make statements if len(discards_34) <= 5: return None central_discards_34 = [x for x in discards_34 if not is_honor(x) and not is_terminal(x)] # also too early to make statements if len(central_discards_34) <= 3: return None # we also want to check how many non-tsumogiri tiles there were after those discards latest_discards_34 = [x.value // 4 for x in discards if not x.is_tsumogiri][-3:] if len(latest_discards_34) != 3: return None # no more than 3, but we expect at least 3 non-central tiles after that one for pattern to matter num_early_discards = min(len(central_discards_34) - 3, 3) first_central_discards_34 = central_discards_34[:num_early_discards] patterns_config = [] if not has_other_danger_bonus: # patterns lowering danger has higher priority in case they are possible # +1 implied here to make it easier to read # order is important, as 28 priority pattern is higher than 37 one patterns_config.extend( [ { "pattern": 2, "danger": [1], "bonus": TileDanger.BONUS_EARLY_28, }, { "pattern": 8, "danger": [9], "bonus": TileDanger.BONUS_EARLY_28, }, { "pattern": 3, "danger": [1, 2], "bonus": TileDanger.BONUS_EARLY_37, }, { "pattern": 7, "danger": [8, 9], "bonus": TileDanger.BONUS_EARLY_37, }, ] ) # patterns increasing danger have lower priority, but are always applied patterns_config.extend( [ { "pattern": 5, "danger": [1, 9], "bonus": TileDanger.BONUS_EARLY_5, }, ] ) # we return the first pattern we see for enemy_discard_34 in first_central_discards_34: # being also discarded late from hand kinda ruins our previous logic, so don't modify danger in that case if enemy_discard_34 in latest_discards_34: continue if not is_tiles_same_suit(enemy_discard_34, tile_analyze_34): continue # +1 here to make it easier read matagi patterns enemy_discard_simplified = simplify(enemy_discard_34) + 1 for pattern_config in patterns_config: has_pattern = enemy_discard_simplified == pattern_config["pattern"] if not has_pattern: continue if tile_analyze_simplified in pattern_config["danger"]: return pattern_config["bonus"] return None
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 _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_assumed_hand_cost_for_riichi( self, tile_136, can_be_used_for_ryanmen) -> int: scale_index = 0 tile_34 = tile_136 // 4 if self.enemy.is_dealer: scale = [ 2900, 5800, 7700, 12000, 12000, 18000, 18000, 24000, 24000, 48000 ] else: scale = [ 2000, 3900, 5200, 8000, 8000, 12000, 12000, 16000, 16000, 32000 ] # it wasn't early riichi, let's think that it could be more expensive if 6 <= self.enemy.riichi_called_on_step <= 11: scale_index += 1 # more late riichi, probably means more expensive riichi if self.enemy.riichi_called_on_step >= 12: scale_index += 2 if self.enemy.is_ippatsu: scale_index += 1 total_dora_in_game = len(self.table.dora_indicators) * 4 + ( 3 * int(self.table.has_aka_dora)) visible_tiles = self.table.revealed_tiles_136 + self.main_player.closed_hand visible_dora_tiles = sum([ plus_dora(x, self.table.dora_indicators, add_aka_dora=self.table.has_aka_dora) for x in visible_tiles ]) live_dora_tiles = total_dora_in_game - visible_dora_tiles assert live_dora_tiles >= 0, "Live dora tiles can't be less than 0" # there are too many live dora tiles, let's increase hand cost if live_dora_tiles >= 4: scale_index += 1 # if we are discarding dora we are obviously going to make enemy hand more expensive scale_index += self._get_dora_scale_bonus(tile_136) # if enemy has closed kan, his hand is more expensive on average for meld in self.enemy.melds: # if he is in riichi he can only have closed kan assert meld.type == Meld.KAN and not meld.opened # plus two just because of riichi with kan scale_index += 2 # higher danger for doras for tile in meld.tiles: scale_index += plus_dora(tile, self.table.dora_indicators, add_aka_dora=self.table.has_aka_dora) # higher danger for yakuhai tile_meld_34 = meld.tiles[0] // 4 scale_index += len( [x for x in self.enemy.valued_honors if x == tile_meld_34]) # let's add more danger for all other opened kan sets on the table for other_player in self.table.players: if other_player.seat == self.enemy.seat: continue for meld in other_player.melds: if meld.type == Meld.KAN or meld.type == Meld.SHOUMINKAN: scale_index += 1 # additional danger for tiles that could be used for tanyao if not is_honor(tile_34): # +1 here to make it more readable simplified_tile = simplify(tile_34) + 1 if simplified_tile in [4, 5, 6]: scale_index += 1 if simplified_tile in [2, 3, 7, 8] and can_be_used_for_ryanmen: scale_index += 1 if scale_index > len(scale) - 1: scale_index = len(scale) - 1 return scale[scale_index]
def _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 calculate_value(self): # base is 100 for ability to mark tiles as not needed (like set value to 50) value = 100 honored_value = 20 if is_honor(self.tile_to_discard): if self.tile_to_discard in self.player.valued_honors: count_of_winds = [x for x in self.player.valued_honors if x == self.tile_to_discard] # for west-west, east-east we had to double tile value value += honored_value * len(count_of_winds) else: # aim for tanyao if self.player.ai.current_strategy and self.player.ai.current_strategy.type == BaseStrategy.TANYAO: suit_tile_grades = [10, 20, 30, 50, 40, 50, 30, 20, 10] # usual hand else: suit_tile_grades = [10, 20, 40, 50, 30, 50, 40, 20, 10] simplified_tile = simplify(self.tile_to_discard) value += suit_tile_grades[simplified_tile] for indicator in self.player.table.dora_indicators: indicator_34 = indicator // 4 if is_honor(indicator_34): continue # indicator and tile not from the same suit if is_sou(indicator_34) and not is_sou(self.tile_to_discard): continue # indicator and tile not from the same suit if is_man(indicator_34) and not is_man(self.tile_to_discard): continue # indicator and tile not from the same suit if is_pin(indicator_34) and not is_pin(self.tile_to_discard): continue simplified_indicator = simplify(indicator_34) simplified_dora = simplified_indicator + 1 # indicator is 9 man if simplified_dora == 9: simplified_dora = 0 # tile so close to the dora if simplified_tile + 1 == simplified_dora or simplified_tile - 1 == simplified_dora: value += DiscardOption.DORA_FIRST_NEIGHBOUR # tile not far away from dora if simplified_tile + 2 == simplified_dora or simplified_tile - 2 == simplified_dora: value += DiscardOption.DORA_SECOND_NEIGHBOUR count_of_dora = plus_dora(self.tile_to_discard * 4, self.player.table.dora_indicators) tile_136 = self.find_tile_in_hand(self.player.closed_hand) if is_aka_dora(tile_136, self.player.table.has_aka_dora): count_of_dora += 1 self.count_of_dora = count_of_dora value += count_of_dora * DiscardOption.DORA_VALUE if is_honor(self.tile_to_discard): # depends on how much honor tiles were discarded # we will decrease tile value discard_percentage = [100, 75, 20, 0, 0] discarded_tiles = self.player.table.revealed_tiles[self.tile_to_discard] value = (value * discard_percentage[discarded_tiles]) / 100 # three honor tiles were discarded, # so we don't need this tile anymore if value == 0: self.had_to_be_discarded = True self.valuation = int(value)
def _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 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 collect_stat_for_enemy_riichi_hand_cost(tile_136, enemy, main_player): tile_34 = tile_136 // 4 riichi_discard = [x for x in enemy.discards if x.riichi_discard] if riichi_discard: assert len(riichi_discard) == 1 riichi_discard = riichi_discard[0] else: # FIXME: it happens when user called riichi and we are trying to decide to we need to open hand on # riichi tile or not. We need to process this situation correctly. riichi_discard = enemy.discards[-1] riichi_called_on_step = enemy.discards.index(riichi_discard) + 1 total_dora_in_game = len(enemy.table.dora_indicators) * 4 + ( 3 * int(enemy.table.has_aka_dora)) visible_tiles = enemy.table.revealed_tiles_136 + main_player.closed_hand visible_dora_tiles = sum([ plus_dora(x, enemy.table.dora_indicators, add_aka_dora=enemy.table.has_aka_dora) for x in visible_tiles ]) live_dora_tiles = total_dora_in_game - visible_dora_tiles assert live_dora_tiles >= 0, "Live dora tiles can't be less than 0" number_of_kan_in_enemy_hand = 0 number_of_dora_in_enemy_kan_sets = 0 number_of_yakuhai_enemy_kan_sets = 0 for meld in enemy.melds: # if he is in riichi he can only have closed kan assert meld.type == MeldPrint.KAN and not meld.opened number_of_kan_in_enemy_hand += 1 for tile in meld.tiles: number_of_dora_in_enemy_kan_sets += plus_dora( tile, enemy.table.dora_indicators, add_aka_dora=enemy.table.has_aka_dora) tile_meld_34 = meld.tiles[0] // 4 if tile_meld_34 in enemy.valued_honors: number_of_yakuhai_enemy_kan_sets += 1 number_of_other_player_kan_sets = 0 for other_player in enemy.table.players: if other_player.seat == enemy.seat: continue for meld in other_player.melds: if meld.type == MeldPrint.KAN or meld.type == MeldPrint.SHOUMINKAN: number_of_other_player_kan_sets += 1 tile_category = "" # additional danger for tiles that could be used for tanyao if not is_honor(tile_34): # +1 here to make it more readable simplified_tile = simplify(tile_34) + 1 if simplified_tile in [4, 5, 6]: tile_category = "middle" if simplified_tile in [2, 3, 7, 8]: tile_category = "edge" if simplified_tile in [1, 9]: tile_category = "terminal" else: tile_category = "honor" if tile_34 in enemy.valued_honors: tile_category = "valuable_honor" return { "is_dealer": enemy.is_dealer and 1 or 0, "riichi_called_on_step": riichi_called_on_step, "current_enemy_step": len(enemy.discards), "wind_number": main_player.table.round_wind_number, "scores": enemy.scores, "is_tsumogiri_riichi": riichi_discard.is_tsumogiri and 1 or 0, "is_oikake_riichi": enemy.is_oikake_riichi and 1 or 0, "is_oikake_riichi_against_dealer_riichi_threat": enemy.is_oikake_riichi_against_dealer_riichi_threat and 1 or 0, "is_riichi_against_open_hand_threat": enemy.is_riichi_against_open_hand_threat and 1 or 0, "number_of_kan_in_enemy_hand": number_of_kan_in_enemy_hand, "number_of_dora_in_enemy_kan_sets": number_of_dora_in_enemy_kan_sets, "number_of_yakuhai_enemy_kan_sets": number_of_yakuhai_enemy_kan_sets, "number_of_other_player_kan_sets": number_of_other_player_kan_sets, "live_dora_tiles": live_dora_tiles, "tile_plus_dora": plus_dora(tile_136, enemy.table.dora_indicators, add_aka_dora=enemy.table.has_aka_dora), "tile_category": tile_category, "discards_before_riichi_34": ";".join([ str(x.value // 4) for x in enemy.discards[:riichi_called_on_step] ]), }
def calculate_valuation(self): # base is 100 for ability to mark tiles as not needed (like set value to 50) value = 100 honored_value = 20 if is_honor(self.tile_to_discard_34): if self.tile_to_discard_34 in self.player.valued_honors: count_of_winds = [ x for x in self.player.valued_honors if x == self.tile_to_discard_34 ] # for west-west, east-east we had to double tile value value += honored_value * len(count_of_winds) else: # aim for tanyao if self.player.ai.current_strategy and self.player.ai.current_strategy.type == BaseStrategy.TANYAO: suit_tile_grades = [10, 20, 30, 50, 40, 50, 30, 20, 10] # usual hand else: suit_tile_grades = [10, 20, 40, 50, 30, 50, 40, 20, 10] simplified_tile = simplify(self.tile_to_discard_34) value += suit_tile_grades[simplified_tile] for indicator in self.player.table.dora_indicators: indicator_34 = indicator // 4 if is_honor(indicator_34): continue # indicator and tile not from the same suit if is_sou(indicator_34) and not is_sou( self.tile_to_discard_34): continue # indicator and tile not from the same suit if is_man(indicator_34) and not is_man( self.tile_to_discard_34): continue # indicator and tile not from the same suit if is_pin(indicator_34) and not is_pin( self.tile_to_discard_34): continue simplified_indicator = simplify(indicator_34) simplified_dora = simplified_indicator + 1 # indicator is 9 man if simplified_dora == 9: simplified_dora = 0 # tile so close to the dora if simplified_tile + 1 == simplified_dora or simplified_tile - 1 == simplified_dora: value += DiscardOption.DORA_FIRST_NEIGHBOUR # tile not far away from dora if simplified_tile + 2 == simplified_dora or simplified_tile - 2 == simplified_dora: value += DiscardOption.DORA_SECOND_NEIGHBOUR count_of_dora = plus_dora(self.tile_to_discard_136, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora) self.count_of_dora = count_of_dora value += count_of_dora * DiscardOption.DORA_VALUE if is_honor(self.tile_to_discard_34): # depends on how much honor tiles were discarded # we will decrease tile value discard_percentage = [100, 75, 20, 0, 0] discarded_tiles = self.player.table.revealed_tiles[ self.tile_to_discard_34] value = (value * discard_percentage[discarded_tiles]) / 100 # three honor tiles were discarded, # so we don't need this tile anymore if value == 0: self.had_to_be_discarded = True self.valuation = int(value)
def _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"]