def calculate_dora_count(self, tiles_136): self.dora_count_central = 0 self.dora_count_not_central = 0 self.aka_dora_count = 0 for tile_136 in tiles_136: tile_34 = tile_136 // 4 dora_count = plus_dora( tile_136, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora ) if not dora_count: continue if is_honor(tile_34): self.dora_count_not_central += dora_count self.dora_count_honor += dora_count elif is_terminal(tile_34): self.dora_count_not_central += dora_count else: self.dora_count_central += dora_count self.dora_count_central += self.aka_dora_count self.dora_count_total = self.dora_count_central + self.dora_count_not_central
def calculate_dora_count(self, tiles_136): self.dora_count_central = 0 self.dora_count_not_central = 0 self.aka_dora_count = 0 for tile_136 in tiles_136: tile_34 = tile_136 // 4 dora_count = plus_dora(tile_136, self.player.table.dora_indicators) if is_aka_dora(tile_136, self.player.table.has_aka_dora): self.aka_dora_count += 1 if not dora_count: continue if is_honor(tile_34): self.dora_count_not_central += dora_count self.dora_count_honor += dora_count elif is_terminal(tile_34): self.dora_count_not_central += dora_count else: self.dora_count_central += dora_count self.dora_count_central += self.aka_dora_count self.dora_count_total = self.dora_count_central + self.dora_count_not_central
def player_can_call_kyuushu_kyuuhai(self, player): if len(player.discards) > 0 or len(player.melds) > 0: return False tiles_34 = [x // 4 for x in player.tiles] terminals_and_honors = [ x for x in tiles_34 if is_honor(x) or is_terminal(x) ] return len(list(set(terminals_and_honors))) >= 9
def _calculate_assumed_hand_cost(self, tile_136) -> int: tile_34 = tile_136 // 4 melds_han = self.get_melds_han(tile_34) if melds_han == 0: return 0 scale_index = melds_han scale_index += self.threat_reason.get("dora_count", 0) scale_index += self._get_dora_scale_bonus(tile_136) if self.enemy.is_dealer: scale = [ 1000, 2900, 5800, 12000, 12000, 18000, 18000, 24000, 24000, 24000, 36000, 36000, 48000 ] else: scale = [ 1000, 2000, 3900, 8000, 8000, 12000, 12000, 16000, 16000, 16000, 24000, 24000, 32000 ] # add more danger for kan sets (basically it is additional hand cost because of fu) for meld in self.enemy.melds: if meld.type != Meld.KAN and meld.type != Meld.SHOUMINKAN: continue if meld.opened: # enemy will get additional fu for opened honors or terminals kan if is_honor(meld.tiles[0] // 4) or is_terminal( meld.tiles[0] // 4): scale_index += 1 else: # enemy will get additional fu for closed kan scale_index += 1 if scale_index > len(scale) - 1: scale_index = len(scale) - 1 elif scale_index == 0: scale_index = 1 return scale[scale_index - 1]
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 _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 _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_tiles_danger( self, discard_candidates: List[DiscardOption], enemy_analyzer: EnemyAnalyzer ) -> List[DiscardOption]: closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) safe_against_threat_34 = [] # First, add all genbutsu to the list safe_against_threat_34.extend(list(set([x for x in enemy_analyzer.enemy.all_safe_tiles]))) # Then add tiles not suitable for yaku in enemy open hand if enemy_analyzer.threat_reason.get("active_yaku"): safe_against_yaku = set.intersection( *[set(x.get_safe_tiles_34()) for x in enemy_analyzer.threat_reason.get("active_yaku")] ) if safe_against_yaku: safe_against_threat_34.extend(list(safe_against_yaku)) possible_forms = self.possible_forms_analyzer.calculate_possible_forms(enemy_analyzer.enemy.all_safe_tiles) kabe_tiles = self.player.ai.kabe.find_all_kabe(closed_hand_34) suji_tiles = self.player.ai.suji.find_suji([x.value for x in enemy_analyzer.enemy.discards]) for discard_option in discard_candidates: tile_34 = discard_option.tile_to_discard_34 tile_136 = discard_option.find_tile_in_hand(self.player.closed_hand) number_of_revealed_tiles = self.player.number_of_revealed_tiles(tile_34, closed_hand_34) # like 1-9 against tanyao etc. if tile_34 in safe_against_threat_34: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.SAFE_AGAINST_THREATENING_HAND, ) continue # safe tiles that can be safe based on the table situation if self.total_possible_forms_for_tile(possible_forms, tile_34) == 0: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.IMPOSSIBLE_WAIT, ) continue # honors if is_honor(tile_34): danger = self._process_danger_for_honor(enemy_analyzer, tile_34, number_of_revealed_tiles) # terminals elif is_terminal(tile_34): danger = self._process_danger_for_terminal_tiles_and_kabe_suji( enemy_analyzer, tile_34, number_of_revealed_tiles, kabe_tiles, suji_tiles ) # 2-8 tiles else: danger = self._process_danger_for_2_8_tiles_suji_and_kabe( enemy_analyzer, tile_34, number_of_revealed_tiles, suji_tiles, kabe_tiles ) if danger: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, danger, ) forms_count = possible_forms[tile_34] self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, { "value": self.possible_forms_analyzer.calculate_possible_forms_danger(forms_count), "description": TileDanger.FORM_BONUS_DESCRIPTION, "forms_count": forms_count, }, ) # for ryanmen waits we also account for number of dangerous suji tiles forms_ryanmen_count = forms_count[PossibleFormsAnalyzer.POSSIBLE_RYANMEN_SIDES] if forms_ryanmen_count == 1: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.RYANMEN_BASE_SINGLE, ) elif forms_ryanmen_count == 2: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.RYANMEN_BASE_DOUBLE, ) if forms_ryanmen_count == 1 or forms_ryanmen_count == 2: has_matagi = self._is_matagi_suji(enemy_analyzer, tile_34) if has_matagi: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.BONUS_MATAGI_SUJI, can_be_used_for_ryanmen=True, ) has_aidayonken = self.is_aidayonken_pattern(enemy_analyzer, tile_34) if has_aidayonken: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.BONUS_AIDAYONKEN, can_be_used_for_ryanmen=True, ) early_danger_bonus = self._get_early_danger_bonus(enemy_analyzer, tile_34, has_matagi or has_aidayonken) if early_danger_bonus is not None: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, early_danger_bonus, can_be_used_for_ryanmen=True, ) self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.make_unverified_suji_coeff(enemy_analyzer.unverified_suji_coeff), can_be_used_for_ryanmen=True, ) if is_dora_connector(tile_136, self.player.table.dora_indicators): self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, TileDanger.DORA_CONNECTOR_BONUS, can_be_used_for_ryanmen=True, ) dora_count = plus_dora( tile_136, self.player.table.dora_indicators, add_aka_dora=self.player.table.has_aka_dora ) if dora_count > 0: danger = copy(TileDanger.DORA_BONUS) danger["value"] = dora_count * danger["value"] danger["dora_count"] = dora_count self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, danger, ) if enemy_analyzer.threat_reason.get("active_yaku"): for yaku_analyzer in enemy_analyzer.threat_reason.get("active_yaku"): bonus_danger = yaku_analyzer.get_bonus_danger(tile_136, number_of_revealed_tiles) for danger in bonus_danger: self._update_discard_candidate( tile_34, discard_candidates, enemy_analyzer.enemy.seat, danger, ) return discard_candidates