def discard_tile(self, tiles, closed_hand, melds, print_log=True): selected_tile = self.choose_tile_to_discard( tiles, closed_hand, melds, print_log=print_log ) # bot think that there is a threat on the table # and better to fold # if we can't find safe tiles, let's continue to build our hand if self.ai.defence.should_go_to_defence_mode(selected_tile): if not self.ai.in_defence: DecisionsLogger.debug(log.DEFENCE_ACTIVATE) self.ai.in_defence = True defence_tile = self.ai.defence.try_to_find_safe_tile_to_discard() if defence_tile: return self.process_discard_option(defence_tile, closed_hand) else: if self.ai.in_defence: DecisionsLogger.debug(log.DEFENCE_DEACTIVATE) self.ai.in_defence = False return self.process_discard_option(selected_tile, closed_hand, print_log=print_log)
def try_to_call_meld(self, tile_136, is_kamicha_discard, remaining_tiles): tiles_136_previous = self.player.tiles[:] tiles_136 = tiles_136_previous + [tile_136] self.determine_strategy(tiles_136) if not self.current_strategy: return None, None tiles_34_previous = TilesConverter.to_34_array(tiles_136_previous) previous_shanten, _ = self.hand_builder.calculate_shanten( tiles_34_previous, self.player.meld_34_tiles) if previous_shanten == Shanten.AGARI_STATE and not self.current_strategy.can_meld_into_agari( ): return None, None meld, discard_option = self.current_strategy.try_to_call_meld( tile_136, is_kamicha_discard, tiles_136, remaining_tiles) if discard_option: self.last_discard_option = discard_option DecisionsLogger.debug( log.MELD_CALL, 'Try to call meld', context=[ 'Hand: {}'.format( self.player.format_hand_for_print(tile_136)), 'Meld: {}'.format(meld), 'Discard after meld: {}'.format(discard_option) ]) return meld, discard_option
def try_to_call_meld(self, tile_136, is_kamicha_discard): tiles_136_previous = self.player.tiles[:] tiles_136 = tiles_136_previous + [tile_136] self.determine_strategy(tiles_136) if not self.current_strategy: return None, None tiles_34_previous = TilesConverter.to_34_array(tiles_136_previous) previous_shanten, _ = self.hand_builder.calculate_shanten(tiles_34_previous, self.player.meld_34_tiles) if previous_shanten == Shanten.AGARI_STATE and not self.current_strategy.can_meld_into_agari(): return None, None meld, discard_option = self.current_strategy.try_to_call_meld(tile_136, is_kamicha_discard, tiles_136) if discard_option: self.last_discard_option = discard_option DecisionsLogger.debug(log.MELD_CALL, 'Try to call meld', context=[ 'Hand: {}'.format(self.player.format_hand_for_print(tile_136)), 'Meld: {}'.format(meld), 'Discard after meld: {}'.format(discard_option) ]) return meld, discard_option
def init_hand(self): DecisionsLogger.debug(log.INIT_HAND, context=[ 'Round wind: {}'.format(DISPLAY_WINDS[self.table.round_wind_tile]), 'Player wind: {}'.format(DISPLAY_WINDS[self.player.player_wind]), 'Hand: {}'.format(self.player.format_hand_for_print()), ]) self.shanten, _ = self.hand_builder.calculate_shanten(TilesConverter.to_34_array(self.player.tiles))
def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ if self.defence.should_go_to_defence_mode(): self.in_defence = True DecisionsLogger.debug(log.DEFENCE_ACTIVATE)
def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand, discarded_tile): discarded_tile_34 = discarded_tile // 4 final_results = [] for meld_34 in possible_melds: meld_34_copy = meld_34.copy() closed_hand_copy = closed_hand.copy() meld_type = is_chi(meld_34_copy) and Meld.CHI or Meld.PON meld_34_copy.remove(discarded_tile_34) first_tile = TilesConverter.find_34_tile_in_136_array( meld_34_copy[0], closed_hand_copy) closed_hand_copy.remove(first_tile) second_tile = TilesConverter.find_34_tile_in_136_array( meld_34_copy[1], closed_hand_copy) closed_hand_copy.remove(second_tile) tiles = [first_tile, second_tile, discarded_tile] meld = Meld() meld.type = meld_type meld.tiles = sorted(tiles) melds = self.player.melds + [meld] selected_tile = self.player.ai.hand_builder.choose_tile_to_discard( new_tiles, closed_hand_copy, melds, print_log=False) final_results.append({ 'discard_tile': selected_tile, 'meld_print': TilesConverter.to_one_line_string( [meld_34[0] * 4, meld_34[1] * 4, meld_34[2] * 4]), 'meld': meld }) final_results = sorted(final_results, key=lambda x: (x['discard_tile'].shanten, -x['discard_tile']. ukeire, x['discard_tile'].valuation)) DecisionsLogger.debug(log.MELD_PREPARE, 'Options with meld calling', context=final_results) return final_results[0]
def _find_best_meld_to_open(self, possible_melds, new_tiles, closed_hand, discarded_tile): discarded_tile_34 = discarded_tile // 4 final_results = [] for meld_34 in possible_melds: meld_34_copy = meld_34.copy() closed_hand_copy = closed_hand.copy() meld_type = is_chi(meld_34_copy) and Meld.CHI or Meld.PON meld_34_copy.remove(discarded_tile_34) first_tile = TilesConverter.find_34_tile_in_136_array(meld_34_copy[0], closed_hand_copy) closed_hand_copy.remove(first_tile) second_tile = TilesConverter.find_34_tile_in_136_array(meld_34_copy[1], closed_hand_copy) closed_hand_copy.remove(second_tile) tiles = [ first_tile, second_tile, discarded_tile ] meld = Meld() meld.type = meld_type meld.tiles = sorted(tiles) melds = self.player.melds + [meld] selected_tile = self.player.ai.hand_builder.choose_tile_to_discard( new_tiles, closed_hand_copy, melds, print_log=False ) final_results.append({ 'discard_tile': selected_tile, 'meld_print': TilesConverter.to_one_line_string([meld_34[0] * 4, meld_34[1] * 4, meld_34[2] * 4]), 'meld': meld }) final_results = sorted(final_results, key=lambda x: (x['discard_tile'].shanten, -x['discard_tile'].ukeire, x['discard_tile'].valuation)) DecisionsLogger.debug(log.MELD_PREPARE, 'Options with meld calling', context=final_results) return final_results[0]
def determine_strategy(self, tiles_136): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False old_strategy = self.current_strategy self.current_strategy = None # order is important, we add strategies with the highest priority first strategies = [] if self.player.table.has_open_tanyao: strategies.append( TanyaoStrategy(BaseStrategy.TANYAO, self.player, self.gpparams)) strategies.append( YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player, self.gpparams)) strategies.append( HonitsuStrategy(BaseStrategy.HONITSU, self.player, self.gpparams)) strategies.append( ChinitsuStrategy(BaseStrategy.CHINITSU, self.player, self.gpparams)) strategies.append( ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, self.player, self.gpparams)) strategies.append( FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player, self.gpparams)) for strategy in strategies: if strategy.should_activate_strategy(tiles_136): self.current_strategy = strategy break if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: DecisionsLogger.debug( log.STRATEGY_ACTIVATE, context=self.current_strategy, ) if not self.current_strategy and old_strategy: DecisionsLogger.debug(log.STRATEGY_DROP, context=old_strategy) return self.current_strategy and True or False
def draw_tile(self, tile_136): DecisionsLogger.debug( log.DRAW, context=[ 'Step: {}'.format(self.round_step), 'Hand: {}'.format(self.format_hand_for_print(tile_136)), 'In defence: {}'.format(self.ai.in_defence), 'Current strategy: {}'.format(self.ai.current_strategy) ]) self.last_draw = tile_136 self.tiles.append(tile_136) # we need sort it to have a better string presentation self.tiles = sorted(self.tiles) self.ai.draw_tile(tile_136)
def draw_tile(self, tile_136): DecisionsLogger.debug( log.DRAW, context=[ 'Step: {}'.format(self.round_step), 'Hand: {}'.format(self.format_hand_for_print(tile_136)), 'In defence: {}'.format(self.ai.in_defence), 'Current strategy: {}'.format(self.ai.current_strategy) ] ) self.last_draw = tile_136 self.tiles.append(tile_136) # we need sort it to have a better string presentation self.tiles = sorted(self.tiles) self.ai.draw_tile(tile_136)
def process_discard_option(self, discard_option, closed_hand, force_discard=False, print_log=True): if print_log: DecisionsLogger.debug( log.DISCARD, context=discard_option ) self.player.in_tempai = discard_option.shanten == 0 self.ai.waiting = discard_option.waiting self.ai.shanten = discard_option.shanten self.ai.ukeire = discard_option.ukeire self.ai.ukeire_second = discard_option.ukeire_second # when we called meld we don't need "smart" discard if force_discard: return discard_option.find_tile_in_hand(closed_hand) last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard: return self.player.last_draw else: return discard_option.find_tile_in_hand(closed_hand)
def determine_strategy(self, tiles_136): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False old_strategy = self.current_strategy self.current_strategy = None # order is important, we add strategies with the highest priority first strategies = [] if self.player.table.has_open_tanyao: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) strategies.append(YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player)) strategies.append(HonitsuStrategy(BaseStrategy.HONITSU, self.player)) strategies.append(ChinitsuStrategy(BaseStrategy.CHINITSU, self.player)) strategies.append(ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, self.player)) strategies.append(FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player)) for strategy in strategies: if strategy.should_activate_strategy(tiles_136): self.current_strategy = strategy break if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: DecisionsLogger.debug( log.STRATEGY_ACTIVATE, context=self.current_strategy, ) if not self.current_strategy and old_strategy: DecisionsLogger.debug(log.STRATEGY_DROP, context=old_strategy) return self.current_strategy and True or False
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