def _initialize_honitsu_dora_count(self, tiles_136, suit): tiles_34 = TilesConverter.to_34_array(tiles_136) dora_count_man_not_isolated = 0 dora_count_pin_not_isolated = 0 dora_count_sou_not_isolated = 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): dora_count += 1 if is_man(tile_34): if not is_tile_strictly_isolated(tiles_34, tile_34): dora_count_man_not_isolated += dora_count if is_pin(tile_34): if not is_tile_strictly_isolated(tiles_34, tile_34): dora_count_pin_not_isolated += dora_count if is_sou(tile_34): if not is_tile_strictly_isolated(tiles_34, tile_34): dora_count_sou_not_isolated += dora_count if suit['name'] == 'pin': self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated elif suit['name'] == 'sou': self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated elif suit['name'] == 'man': self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated
def _choose_best_discard_with_4_or_more_shanten(self, discard_options): discard_options = sorted(discard_options, key=lambda x: (x.shanten, -x.ukeire)) first_option = discard_options[0] # we filter by ukeire ukeire_borders = self._choose_ukeire_borders( first_option, DiscardOption.UKEIRE_FIRST_FILTER_PERCENTAGE, "ukeire") possible_options = self._filter_list_by_ukeire_borders( discard_options, first_option.ukeire, ukeire_borders) possible_options = sorted(possible_options, key=self._sorting_rule_for_4_or_more_shanten) possible_options = self._try_keep_doras( possible_options, "ukeire", DiscardOption.UKEIRE_FIRST_FILTER_PERCENTAGE) assert possible_options closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) isolated_tiles = [ x for x in possible_options if is_tile_strictly_isolated(closed_hand_34, x.tile_to_discard_34) ] # isolated tiles should be discarded first if isolated_tiles: possible_options = isolated_tiles # let's sort tiles by value and let's choose less valuable tile to discard return self._choose_best_tile_considering_threats( sorted(possible_options, key=self._sorting_rule_for_4_or_more_shanten), self._sorting_rule_for_4_or_more_shanten, )
def _sorting_rule_for_2_3_shanten_with_isolated(x, closed_hand_34): return ( -x.ukeire_second, -x.ukeire, -is_tile_strictly_isolated(closed_hand_34, x.tile_to_discard_34), x.valuation, )
def calculate_waits(self, closed_hand_34: List[int], all_tiles_34: List[int], use_chiitoitsu: bool = False): previous_shanten = self.ai.calculate_shanten_or_get_from_cache(closed_hand_34, use_chiitoitsu=use_chiitoitsu) waiting = [] for tile_index in range(0, 34): # it is important to check that we don't have all 4 tiles in the all tiles # not in the closed hand # so, we will not count 1z as waiting when we have 111z meld if all_tiles_34[tile_index] == 4: continue closed_hand_34[tile_index] += 1 skip_isolated_tile = True if closed_hand_34[tile_index] == 4: skip_isolated_tile = False if use_chiitoitsu and closed_hand_34[tile_index] == 3: skip_isolated_tile = False # there is no need to check single isolated tile if skip_isolated_tile and is_tile_strictly_isolated(closed_hand_34, tile_index): closed_hand_34[tile_index] -= 1 continue new_shanten = self.ai.calculate_shanten_or_get_from_cache(closed_hand_34, use_chiitoitsu=use_chiitoitsu) if new_shanten == previous_shanten - 1: waiting.append(tile_index) closed_hand_34[tile_index] -= 1 return waiting, previous_shanten
def _initialize_honitsu_dora_count(self, tiles_136, suit): tiles_34 = TilesConverter.to_34_array(tiles_136) dora_count_man = 0 dora_count_pin = 0 dora_count_sou = 0 dora_count_man_not_isolated = 0 dora_count_pin_not_isolated = 0 dora_count_sou_not_isolated = 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 is_man(tile_34): dora_count_man += dora_count if not is_tile_strictly_isolated(tiles_34, tile_34): dora_count_man_not_isolated += dora_count if is_pin(tile_34): dora_count_pin += dora_count if not is_tile_strictly_isolated(tiles_34, tile_34): dora_count_pin_not_isolated += dora_count if is_sou(tile_34): dora_count_sou += dora_count if not is_tile_strictly_isolated(tiles_34, tile_34): dora_count_sou_not_isolated += dora_count if suit["name"] == "pin": self.dora_count_our_suit = dora_count_pin self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_sou_not_isolated elif suit["name"] == "sou": self.dora_count_our_suit = dora_count_sou self.dora_count_other_suits_not_isolated = dora_count_man_not_isolated + dora_count_pin_not_isolated elif suit["name"] == "man": self.dora_count_our_suit = dora_count_man self.dora_count_other_suits_not_isolated = dora_count_sou_not_isolated + dora_count_pin_not_isolated
def _calculate_not_suitable_tiles_cnt(self, tiles_34, suit): self.tiles_count_other_suits = 0 self.tiles_count_other_suits_not_isolated = 0 for x in range(0, 34): tile = tiles_34[x] if not tile: continue if not suit(x) and not is_honor(x): self.tiles_count_other_suits += tile if not is_tile_strictly_isolated(tiles_34, x): self.tiles_count_other_suits_not_isolated += tile
def should_activate_strategy(self, tiles_136, meld_tile=None): """ Tanyao hand is a hand without terminal and honor tiles, to achieve this we will use different approaches :return: boolean """ result = super(TanyaoStrategy, self).should_activate_strategy(tiles_136) if not result: return False tiles = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) isolated_tiles = [ x // 4 for x in self.player.tiles if is_tile_strictly_isolated(closed_hand_34, x // 4) or is_honor(x // 4) ] count_of_terminal_pon_sets = 0 count_of_terminal_pairs = 0 count_of_valued_pairs = 0 count_of_not_suitable_tiles = 0 count_of_not_suitable_not_isolated_tiles = 0 for x in range(0, 34): tile = tiles[x] if not tile: continue if x in self.not_suitable_tiles and tile == 3: count_of_terminal_pon_sets += 1 if x in self.not_suitable_tiles and tile == 2: count_of_terminal_pairs += 1 if x in self.player.valued_honors: count_of_valued_pairs += 1 if x in self.not_suitable_tiles: count_of_not_suitable_tiles += tile if x in self.not_suitable_tiles and x not in isolated_tiles: count_of_not_suitable_not_isolated_tiles += tile # we have too much terminals and honors if count_of_not_suitable_tiles >= 5: return False # if we already have pon of honor\terminal tiles # we don't need to open hand for tanyao if count_of_terminal_pon_sets > 0: return False # with valued pair (yakuhai wind or dragon) # we don't need to go for tanyao if count_of_valued_pairs > 0: return False # one pair is ok in tanyao pair # but 2+ pairs can't be suitable if count_of_terminal_pairs > 1: return False # 3 or more not suitable tiles that # are not isolated is too much if count_of_not_suitable_not_isolated_tiles >= 3: return False # if we are 1 shanten, even 2 tiles # that are not suitable and not isolated # is too much if count_of_not_suitable_not_isolated_tiles >= 2 and self.player.ai.shanten == 1: return False # TODO: don't open from good 1-shanten into tanyao 1-shaten with same ukeire or worse # 123 and 789 indices indices = [[0, 1, 2], [6, 7, 8], [9, 10, 11], [15, 16, 17], [18, 19, 20], [24, 25, 26]] for index_set in indices: first = tiles[index_set[0]] second = tiles[index_set[1]] third = tiles[index_set[2]] if first >= 1 and second >= 1 and third >= 1: return False # if we have 2 or more non-central doras # we don't want to go for tanyao if self.dora_count_not_central >= 2: return False # if we have less than two central doras # let's not consider open tanyao if self.dora_count_central < 2: return False # if we have only two central doras let's # wait for 5th turn before opening our hand if self.dora_count_central == 2 and self.player.round_step < 5: return False return True
def test_is_strictly_isolated_tile(self): hand_34 = TilesConverter.string_to_34_array(sou='1399', pin='1567', man='25', honors='1224') self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou='1')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou='2')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou='3')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou='4')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou='5')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou='6')), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou='7')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou='8')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou='9')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin='1')), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin='2')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin='3')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin='4')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin='5')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin='6')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin='7')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin='8')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin='9')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man='1')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man='2')), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man='3')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man='4')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man='5')), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man='6')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man='7')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man='8')), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man='9')), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors='1')), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors='2')), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors='3')), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors='4')), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors='5')), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors='6')), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors='7')), True)
def choose_tile_to_discard(self, tiles, closed_hand, melds, print_log=True): """ Try to find best tile to discard, based on different rules """ discard_options, _ = self.find_discard_options( tiles, closed_hand, melds ) # our strategy can affect discard options if self.ai.current_strategy: discard_options = self.ai.current_strategy.determine_what_to_discard( discard_options, closed_hand, melds ) had_to_be_discarded_tiles = [x for x in discard_options if x.had_to_be_discarded] if had_to_be_discarded_tiles: discard_options = sorted(had_to_be_discarded_tiles, key=lambda x: (x.shanten, -x.ukeire, x.valuation)) DecisionsLogger.debug( log.DISCARD_OPTIONS, 'Discard marked tiles first', discard_options, print_log=print_log ) return discard_options[0] # remove needed tiles from discard options discard_options = [x for x in discard_options if not x.had_to_be_saved] discard_options = sorted(discard_options, key=lambda x: (x.shanten, -x.ukeire)) first_option = discard_options[0] results_with_same_shanten = [x for x in discard_options if x.shanten == first_option.shanten] possible_options = [first_option] ukeire_borders = self._choose_ukeire_borders(first_option, 20, 'ukeire') for discard_option in results_with_same_shanten: # there is no sense to check already chosen tile if discard_option.tile_to_discard == first_option.tile_to_discard: continue # let's choose tiles that are close to the max ukeire tile if discard_option.ukeire >= first_option.ukeire - ukeire_borders: possible_options.append(discard_option) if first_option.shanten in [1, 2, 3]: ukeire_field = 'ukeire_second' for x in possible_options: self.calculate_second_level_ukeire(x, tiles, melds) possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field)) filter_percentage = 20 possible_options = self._filter_list_by_percentage( possible_options, ukeire_field, filter_percentage ) else: ukeire_field = 'ukeire' possible_options = sorted(possible_options, key=lambda x: -getattr(x, ukeire_field)) # only one option - so we choose it if len(possible_options) == 1: return possible_options[0] # tempai state has a special handling if first_option.shanten == 0: other_tiles_with_same_shanten = [x for x in possible_options if x.shanten == 0] return self._choose_best_discard_in_tempai(tiles, melds, other_tiles_with_same_shanten) tiles_without_dora = [x for x in possible_options if x.count_of_dora == 0] # we have only dora candidates to discard if not tiles_without_dora: DecisionsLogger.debug( log.DISCARD_OPTIONS, context=possible_options, print_log=print_log ) min_dora = min([x.count_of_dora for x in possible_options]) min_dora_list = [x for x in possible_options if x.count_of_dora == min_dora] return sorted(min_dora_list, key=lambda x: -getattr(x, ukeire_field))[0] # only one option - so we choose it if len(tiles_without_dora) == 1: return tiles_without_dora[0] # 1-shanten hands have special handling - we can consider future hand cost here if first_option.shanten == 1: return sorted(tiles_without_dora, key=lambda x: (-x.second_level_cost, -x.ukeire_second, x.valuation))[0] if first_option.shanten == 2 or first_option.shanten == 3: # we filter 10% of options here second_filter_percentage = 10 filtered_options = self._filter_list_by_percentage( tiles_without_dora, ukeire_field, second_filter_percentage ) # we should also consider borders for 3+ shanten hands else: best_option_without_dora = tiles_without_dora[0] ukeire_borders = self._choose_ukeire_borders(best_option_without_dora, 10, ukeire_field) filtered_options = [] for discard_option in tiles_without_dora: val = getattr(best_option_without_dora, ukeire_field) - ukeire_borders if getattr(discard_option, ukeire_field) >= val: filtered_options.append(discard_option) DecisionsLogger.debug( log.DISCARD_OPTIONS, context=possible_options, print_log=print_log ) closed_hand_34 = TilesConverter.to_34_array(closed_hand) isolated_tiles = [x for x in filtered_options if is_tile_strictly_isolated(closed_hand_34, x.tile_to_discard)] # isolated tiles should be discarded first if isolated_tiles: # let's sort tiles by value and let's choose less valuable tile to discard return sorted(isolated_tiles, key=lambda x: x.valuation)[0] # there are no isolated tiles or we don't care about them # let's discard tile with greater ukeire/ukeire2 filtered_options = sorted(filtered_options, key=lambda x: -getattr(x, ukeire_field)) first_option = filtered_options[0] other_tiles_with_same_ukeire = [x for x in filtered_options if getattr(x, ukeire_field) == getattr(first_option, ukeire_field)] # it will happen with shanten=1, all tiles will have ukeire_second == 0 # or in tempai we can have several tiles with same ukeire if other_tiles_with_same_ukeire: return sorted(other_tiles_with_same_ukeire, key=lambda x: x.valuation)[0] # we have only one candidate to discard with greater ukeire return first_option
def test_is_strictly_isolated_tile(self): hand_34 = TilesConverter.string_to_34_array(sou="1399", pin="1567", man="25", honors="1224") self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou="1")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou="2")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou="3")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou="4")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou="5")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou="6")), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou="7")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou="8")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(sou="9")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin="1")), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin="2")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin="3")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin="4")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin="5")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin="6")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin="7")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin="8")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(pin="9")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man="1")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man="2")), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man="3")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man="4")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man="5")), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man="6")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man="7")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man="8")), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(man="9")), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors="1")), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors="2")), False) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors="3")), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors="4")), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors="5")), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors="6")), True) self.assertEqual( is_tile_strictly_isolated(hand_34, self._string_to_34_tile(honors="7")), True)