class ImplementationAI(InterfaceAI): """ AI that will discard tiles to maximize expected shanten. Assumes that tiles are drawn randomly from those not on the table or in hand - aka not revealed to player. Does not account for hidden tiles in opponent's hands. Always calls wins, never calls riichi. TODO: Everything """ version = 'shantenMCS' shanten = None agari = None shdict = {} def __init__(self, player): super(ImplementationAI, self).__init__(player) self.shanten = Shanten() self.hand_divider = HandDivider() self.agari = Agari() self.iterations = 200 def simulate_single(self, hand, hand_open, unaccounted_tiles): """ #simulates a single random draw and calculates shanten hand, hand_open -- hand in 34 format unaccounted_tiles -- all the unused tiles in 34 format turn -- a number from 0-3 (0 is the player) """ hand = list(hand) unaccounted = list(unaccounted_tiles) #14 in dead wall 13*3= 39 in other hand -> total 53 unaccounted_nonzero = np.nonzero(unaccounted)[0] #get a random card draw_tile = random.choice(unaccounted_nonzero) unaccounted[draw_tile] -= 1 hand[draw_tile] += 1 return self.shanten.calculate_shanten(hand, hand_open) # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables def discard_tile(self, discard_tile): if discard_tile is not None: return discard_tile tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) results = [] for tile in range(0, 34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand tiles_34[tile] -= 1 # calculate shanten and store shanten = self.shanten.calculate_shanten( tiles_34, self.player.open_hand_34_tiles) results.append((shanten, tile)) # return tile to hand tiles_34[tile] += 1 (minshanten, discard_34) = min(results) results2 = [] unaccounted = (np.array([4]*34) - closed_tiles_34)\ - TilesConverter.to_34_array(self.table.revealed_tiles) self.shdict = {} for shanten, tile in results: if shanten != minshanten: continue h = sum( self.simulate_single(closed_tiles_34, self.player. open_hand_34_tiles, unaccounted) for _ in range(self.iterations)) / self.iterations results2.append((h, tile)) (h, discard_34) = min(results2) discard_136 = TilesConverter.find_34_tile_in_136_array( discard_34, self.player.closed_hand) if discard_136 is None: logger.debug('Failure') discard_136 = random.randrange(len(self.player.tiles) - 1) discard_136 = self.player.tiles[discard_136] logger.info('Shanten after discard:' + str(shanten)) logger.info('Discard heuristic:' + str(h)) return discard_136 # UNUSED def calculate_outs(self, discard_34, shanten, depth=2): closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) table_34 = list(self.table.revealed_tiles) tiles_34 = TilesConverter.to_34_array(self.player.tiles) table_34[discard_34] += 1 closed_tiles_34[discard_34] -= 1 tiles_34[discard_34] -= 1 hidden_34 = np.array( [4] * 34) - np.array(closed_tiles_34) - np.array(table_34) # print(hidden_34) # want to sample? use this # reveal_num = sum(hidden_34) # draw_p = [float(i)/reveal_num for i in hidden_34] # draw = np.random.choice(34, p=draw_p) return self.out_search(tiles_34, closed_tiles_34, hidden_34, depth, shanten - 1) def calculate_outs_meld(self, discard_34, shanten, tiles_34, closed_tiles_34, open_hand_34, depth=2): table_34 = list(self.table.revealed_tiles) tiles_34 = copy.deepcopy(tiles_34) closed_tiles_34 = copy.deepcopy(closed_tiles_34) table_34[discard_34] += 1 closed_tiles_34[discard_34] -= 1 tiles_34[discard_34] -= 1 hidden_34 = np.array( [4] * 34) - np.array(closed_tiles_34) - np.array(table_34) return self.out_search(tiles_34, closed_tiles_34, hidden_34, depth, shanten - 1, open_hand_34) def out_search(self, tiles_34, closed_tiles_34, hidden_34, depth, shanten, open_hand_34=None): outs = 0 for i in range(34): if hidden_34[i] <= 0: continue ct = hidden_34[i] # draw tile from hidden to concealed hand hidden_34[i] -= 1 closed_tiles_34[i] += 1 tiles_34[i] += 1 if self.agari.is_agari(tiles_34, open_hand_34): outs += 2 else: for tile in range(0, 34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand closed_tiles_34[tile] -= 1 tiles_34[tile] -= 1 tuple_34 = tuple(tiles_34) # calculate shanten and add outs if appropriate if tuple_34 in self.shdict.keys(): sh = self.shdict[tuple_34] else: if open_hand_34 is None: sh = self.shanten.calculate_shanten( tiles_34, self.player.open_hand_34_tiles) else: sh = self.shanten.calculate_shanten( tiles_34, open_hand_34) self.shdict[tuple_34] = sh if sh == shanten: if depth <= 1 or shanten == -1: outs += 1 else: outs += ct * self.out_search( tiles_34, closed_tiles_34, hidden_34, depth - 1, shanten - 1) if sh == shanten + 1: outs += 0.01 # return tile to hand closed_tiles_34[tile] += 1 tiles_34[tile] += 1 # return tile from closed hand to hidden hidden_34[i] += 1 closed_tiles_34[i] -= 1 tiles_34[i] -= 1 return outs def should_call_riichi(self): # if len(self.player.open_hand_34_tiles) != 0: # return False return True # tiles_34 = TilesConverter.to_34_array(self.player.tiles) # shanten = self.shanten.calculate_shanten(tiles_34, None) # logger.debug('Riichi check, shanten = ' + str(shanten)) # return shanten == 0 def should_call_win(self, tile, enemy_seat): return True def should_call_kan(self, tile, open_kan): """ When bot can call kan or chankan this method will be called :param tile: 136 tile format :param is_open_kan: boolean :return: kan type (Meld.KAN, Meld.CHANKAN) or None """ if open_kan: # don't start open hand from called kan if not self.player.is_open_hand: return None # don't call open kan if not waiting for win if not self.player.in_tempai: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # upgrade open pon to kan if possible if pon_melds: for meld in pon_melds: if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # check for improvement in shanten if new_shanten <= previous_shanten: return Meld.KAN return None def try_to_call_meld(self, tile, is_kamicha_discard): """ When bot can open hand with a set (chi or pon/kan) this method will be called :param tile: 136 format tile :param is_kamicha_discard: boolean :return: Meld and DiscardOption objects or None, None """ # can't call if in riichi if self.player.in_riichi: return None, None closed_hand = self.player.closed_hand[:] # check for appropriate hand size, seems to solve a bug if len(closed_hand) == 1: return None, None # get old shanten value old_tiles_34 = TilesConverter.to_34_array(self.player.tiles) old_shanten = self.shanten.calculate_shanten( old_tiles_34, self.player.open_hand_34_tiles) # setup discarded_tile = tile // 4 new_closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile]) # We will use hand_divider to find possible melds involving the discarded tile. # Check its suit and number to narrow the search conditions # skipping this will break the default mahjong functions combinations = [] first_index = 0 second_index = 0 if is_man(discarded_tile): first_index = 0 second_index = 8 elif is_pin(discarded_tile): first_index = 9 second_index = 17 elif is_sou(discarded_tile): first_index = 18 second_index = 26 if second_index == 0: # honor tiles if new_closed_hand_34[discarded_tile] == 3: combinations = [[[discarded_tile] * 3]] else: # to avoid not necessary calculations # we can check only tiles around +-2 discarded tile first_limit = discarded_tile - 2 if first_limit < first_index: first_limit = first_index second_limit = discarded_tile + 2 if second_limit > second_index: second_limit = second_index combinations = self.hand_divider.find_valid_combinations( new_closed_hand_34, first_limit, second_limit, True) # Reduce combinations to list of melds if combinations: combinations = combinations[0] # Verify that a meld can be called possible_melds = [] for meld_34 in combinations: # we can call pon from everyone if is_pon(meld_34) and discarded_tile in meld_34: if meld_34 not in possible_melds: possible_melds.append(meld_34) # we can call chi only from left player if is_chi(meld_34 ) and is_kamicha_discard and discarded_tile in meld_34: if meld_34 not in possible_melds: possible_melds.append(meld_34) # For each possible meld, check if calling it and discarding can improve shanten new_shanten = float('inf') discard_136 = None tiles = None for meld_34 in possible_melds: shanten, disc = self.meldDiscard(meld_34, tile) if shanten < new_shanten: new_shanten, discard_136 = shanten, disc tiles = meld_34 # If shanten can be improved by calling meld, call it if new_shanten < old_shanten: meld = Meld() meld.type = is_chi(tiles) and Meld.CHI or Meld.PON # convert meld tiles back to 136 format for Meld type return # find them in a copy of the closed hand and remove tiles.remove(discarded_tile) first_tile = TilesConverter.find_34_tile_in_136_array( tiles[0], closed_hand) closed_hand.remove(first_tile) second_tile = TilesConverter.find_34_tile_in_136_array( tiles[1], closed_hand) closed_hand.remove(second_tile) tiles_136 = [first_tile, second_tile, tile] discard_136 = TilesConverter.find_34_tile_in_136_array( discard_136 // 4, closed_hand) meld.tiles = sorted(tiles_136) return meld, discard_136 return None, None # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables def meldDiscard(self, meld_34, discardtile): tiles_34 = TilesConverter.to_34_array(self.player.tiles + [discardtile]) closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand + [discardtile]) open_hand_34 = copy.deepcopy(self.player.open_hand_34_tiles) # remove meld from closed and and add to open hand open_hand_34.append(meld_34) for tile_34 in meld_34: closed_tiles_34[tile_34] -= 1 results = [] for tile in range(0, 34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand tiles_34[tile] -= 1 # calculate shanten and store shanten = self.shanten.calculate_shanten(tiles_34, open_hand_34) results.append((shanten, tile)) # return tile to hand tiles_34[tile] += 1 (minshanten, discard_34) = min(results) results2 = [] unaccounted = (np.array([4]*34) - closed_tiles_34)\ - TilesConverter.to_34_array(self.table.revealed_tiles) self.shdict = {} for shanten, tile in results: if shanten != minshanten: continue h = sum( self.simulate_single(closed_tiles_34, open_hand_34, unaccounted) for _ in range(self.iterations)) / self.iterations results2.append((h, tile)) (h, discard_34) = min(results2) discard_136 = TilesConverter.find_34_tile_in_136_array( discard_34, self.player.closed_hand) return minshanten, discard_136
class ImplementationAI(InterfaceAI): """ AI that will discard tiles as to minimize shanten, using perfect shanten calculation. Picks the first tile with resulting in the lowest shanten value when choosing what to discard. Calls riichi if possible and hand is closed. Always calls wins. Calls kan to upgrade pon or on equivalent or reduced shanten. Calls melds to reduce shanten. """ version = 'shantenNaive' shanten = None agari = None def __init__(self, player): super(ImplementationAI, self).__init__(player) self.shanten = Shanten() # self.agari = Agari() self.hand_divider = HandDivider() # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables def discard_tile(self, discard_tile): if discard_tile is not None: return discard_tile tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) # is_agari = self.agari.is_agari(tiles_34, self.player.open_hand_34_tiles) results = [] for tile in range(0,34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand tiles_34[tile] -= 1 # calculate shanten and store shanten = self.shanten.calculate_shanten(tiles_34, self.player.open_hand_34_tiles) results.append((shanten, tile)) # return tile to hand tiles_34[tile] += 1 (shanten, discard_34) = min(results) discard_136 = TilesConverter.find_34_tile_in_136_array(discard_34, self.player.closed_hand) if discard_136 is None: logger.debug('Greedy search or tile conversion failed') discard_136 = random.randrange(len(self.player.tiles) - 1) discard_136 = self.player.tiles[discard_136] logger.info('Shanten after discard:' + str(shanten)) return discard_136 def should_call_riichi(self): return True def should_call_win(self, tile, enemy_seat): return True def should_call_kan(self, tile, open_kan): """ When bot can call kan or chankan this method will be called :param tile: 136 tile format :param is_open_kan: boolean :return: kan type (Meld.KAN, Meld.CHANKAN) or None """ if open_kan: # don't start open hand from called kan if not self.player.is_open_hand: return None # don't call open kan if not waiting for win if not self.player.in_tempai: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # upgrade open pon to kan if possible if pon_melds: for meld in pon_melds: if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # check for improvement in shanten if new_shanten <= previous_shanten: return Meld.KAN return None def try_to_call_meld(self, tile, is_kamicha_discard): """ When bot can open hand with a set (chi or pon/kan) this method will be called :param tile: 136 format tile :param is_kamicha_discard: boolean :return: Meld and DiscardOption objects or None, None """ # can't call if in riichi if self.player.in_riichi: return None, None closed_hand = self.player.closed_hand[:] # check for appropriate hand size, seems to solve a bug if len(closed_hand) == 1: return None, None # get old shanten value old_tiles_34 = TilesConverter.to_34_array(self.player.tiles) old_shanten = self.shanten.calculate_shanten(old_tiles_34, self.player.open_hand_34_tiles) # setup discarded_tile = tile // 4 new_closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile]) # We will use hand_divider to find possible melds involving the discarded tile. # Check its suit and number to narrow the search conditions # skipping this will break the default mahjong functions combinations = [] first_index = 0 second_index = 0 if is_man(discarded_tile): first_index = 0 second_index = 8 elif is_pin(discarded_tile): first_index = 9 second_index = 17 elif is_sou(discarded_tile): first_index = 18 second_index = 26 if second_index == 0: # honor tiles if new_closed_hand_34[discarded_tile] == 3: combinations = [[[discarded_tile] * 3]] else: # to avoid not necessary calculations # we can check only tiles around +-2 discarded tile first_limit = discarded_tile - 2 if first_limit < first_index: first_limit = first_index second_limit = discarded_tile + 2 if second_limit > second_index: second_limit = second_index combinations = self.hand_divider.find_valid_combinations(new_closed_hand_34, first_limit, second_limit, True) # Reduce combinations to list of melds if combinations: combinations = combinations[0] # Verify that a meld can be called possible_melds = [] for meld_34 in combinations: # we can call pon from everyone if is_pon(meld_34) and discarded_tile in meld_34: if meld_34 not in possible_melds: possible_melds.append(meld_34) # we can call chi only from left player if is_chi(meld_34) and is_kamicha_discard and discarded_tile in meld_34: if meld_34 not in possible_melds: possible_melds.append(meld_34) # For each possible meld, check if calling it and discarding can improve shanten new_shanten = float('inf') discard_136 = None tiles = None for meld_34 in possible_melds: shanten, disc = self.meldDiscard(meld_34, tile) if shanten < new_shanten: new_shanten, discard_136 = shanten, disc tiles = meld_34 # If shanten can be improved by calling meld, call it if new_shanten < old_shanten: meld = Meld() meld.type = is_chi(tiles) and Meld.CHI or Meld.PON # convert meld tiles back to 136 format for Meld type return # find them in a copy of the closed hand and remove tiles.remove(discarded_tile) first_tile = TilesConverter.find_34_tile_in_136_array(tiles[0], closed_hand) closed_hand.remove(first_tile) second_tile = TilesConverter.find_34_tile_in_136_array(tiles[1], closed_hand) closed_hand.remove(second_tile) tiles_136 = [ first_tile, second_tile, tile ] discard_136 = TilesConverter.find_34_tile_in_136_array(discard_136 // 4, closed_hand) meld.tiles = sorted(tiles_136) return meld, discard_136 return None, None # TODO: Merge all discard functions into one to prevent code reuse and unnecessary duplication of variables def meldDiscard(self, meld_34, discardtile): tiles_34 = TilesConverter.to_34_array(self.player.tiles + [discardtile]) closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand + [discardtile]) open_hand_34 = copy.deepcopy(self.player.open_hand_34_tiles) # remove meld from closed and and add to open hand open_hand_34.append(meld_34) for tile_34 in meld_34: closed_tiles_34[tile_34] -= 1 results = [] for tile in range(0, 34): # Can the tile be discarded from the concealed hand? if not closed_tiles_34[tile]: continue # discard the tile from hand tiles_34[tile] -= 1 # calculate shanten and store shanten = self.shanten.calculate_shanten(tiles_34, open_hand_34) results.append((shanten, tile)) # return tile to hand tiles_34[tile] += 1 (shanten, discard_34) = min(results) discard_136 = TilesConverter.find_34_tile_in_136_array(discard_34, self.player.closed_hand) return shanten, discard_136
class Phoenix: def __init__(self, player): self.player = player self.table = player.table self.chi = Chi(player) self.pon = Pon(player) self.kan = Kan(player) self.riichi = Riichi(player) self.discard = Discard(player) self.grp = GlobalRewardPredictor() self.hand_builder = HandBuilder(player, self) self.shanten_calculator = Shanten() self.hand_cache_shanten = {} self.placement = player.config.PLACEMENT_HANDLER_CLASS(player) self.finished_hand = HandCalculator() self.hand_divider = HandDivider() self.erase_state() def erase_state(self): self.hand_cache_shanten = {} self.hand_cache_estimation = {} self.finished_hand = HandCalculator() self.grp_features = [] def collect_experience(self): #collect round info init_scores = np.array(self.table.init_scores) / 1e5 gains = np.array(self.table.gains) / 1e5 dans = np.array( [RANKS.index(p.rank) for p in self.player.table.players]) dealer = int(self.player.dealer_seat) repeat_dealer = self.player.table.count_of_honba_sticks riichi_bets = self.player.table.count_of_riichi_sticks features = np.concatenate( (init_scores, gains, dans, np.array([dealer, repeat_dealer, riichi_bets])), axis=0) self.grp_features.append(features) #prepare input grp_input = [np.zeros(pred_emb_dim)] * max( round_num - len(self.grp_features), 0) + self.grp_features[:] reward = self.grp.get_global_reward( np.expand_dims(np.asarray(grp_input), axis=0))[0][self.player.seat] for model in [self.chi, self.pon, self.kan, self.riichi, self.discard]: model.collector.complete_episode(reward) def write_buffer(self): for model in [self.chi, self.pon, self.kan, self.riichi, self.discard]: model.collector.to_buffer() def init_hand(self): self.player.logger.debug( log.INIT_HAND, context=[ f"Round wind: {DISPLAY_WINDS[self.table.round_wind_tile]}", f"Player wind: {DISPLAY_WINDS[self.player.player_wind]}", f"Hand: {self.player.format_hand_for_print()}", ], ) self.shanten, _ = self.hand_builder.calculate_shanten_and_decide_hand_structure( TilesConverter.to_34_array(self.player.tiles)) def draw_tile(self): pass def discard_tile(self, discard_tile): ''' return discarded_tile and with_riichi ''' if discard_tile is not None: #discard after meld return discard_tile, False if self.player.is_open_hand: #can not riichi return self.discard.discard_tile(), False shanten = self.calculate_shanten_or_get_from_cache( TilesConverter.to_34_array(self.player.closed_hand)) if shanten != 0: #can not riichi return self.discard.discard_tile(), False with_riichi, p = self.riichi.should_call_riichi() if with_riichi: # fix here: might need review riichi_options = [ tile for tile in self.player.closed_hand if self.calculate_shanten_or_get_from_cache( TilesConverter.to_34_array( [t for t in self.player.closed_hand if t != tile])) == 0 ] tile_to_discard = self.discard.discard_tile( with_riichi_options=riichi_options) else: tile_to_discard = self.discard.discard_tile() return tile_to_discard, with_riichi def try_to_call_meld(self, tile_136, is_kamicha_discard, meld_type): # 1 pon # 2 kan (it is a closed kan and can be send only to the self draw) # 4 chi # there is two return value, meldPrint() and discardOption(), # while the second would not be used by client.py meld_chi, meld_pon = None, None should_chi, should_pon = False, False # print(tile_136) # print(self.player.closed_hand) melds_chi, melds_pon = self.get_possible_meld(tile_136, is_kamicha_discard) if melds_chi and meld_type & 4: should_chi, chi_score, tiles_chi = self.chi.should_call_chi( tile_136, melds_chi) # fix here: tiles_chi is now the first possible meld ---fixed! # tiles_chi = melds_chi[0] meld_chi = Meld(meld_type="chi", tiles=tiles_chi) if meld_chi else None if melds_pon and meld_type & 1: should_pon, pon_score = self.pon.should_call_pon( tile_136, is_kamicha_discard) tiles_pon = melds_pon[0] meld_pon = Meld(meld_type="pon", tiles=tiles_pon) if meld_pon else None if not should_chi and not should_pon: return None, None if should_chi and should_pon: meld = meld_chi if chi_score > pon_score else meld_pon elif should_chi: meld = meld_chi else: meld = meld_pon all_tiles_copy, meld_tiles_copy = self.player.tiles[:], self.player.meld_tiles[:] all_tiles_copy.append(tile_136) meld_tiles_copy.append(meld) closed_hand_copy = [ item for item in all_tiles_copy if item not in meld_tiles_copy ] discard_option = self.discard.discard_tile( all_hands_136=all_tiles_copy, closed_hands_136=closed_hand_copy) return meld, discard_option def should_call_kyuushu_kyuuhai(self): #try kokushi strategy if with 10 types tiles_34 = TilesConverter.to_34_array(self.player.tiles) types = sum([1 for t in tiles_34 if t > 0]) if types >= 10: return False else: return True def should_call_win(self, tile, is_tsumo, enemy_seat=None, is_chankan=False): # don't skip win in riichi if self.player.in_riichi: return True # currently we don't support win skipping for tsumo if is_tsumo: return True # fast path - check it first to not calculate hand cost cost_needed = self.placement.get_minimal_cost_needed() if cost_needed == 0: return True # 1 and not 0 because we call check for win this before updating remaining tiles is_hotei = self.player.table.count_of_remaining_tiles == 1 hand_response = self.calculate_exact_hand_value_or_get_from_cache( tile, tiles=self.player.tiles, call_riichi=self.player.in_riichi, is_tsumo=is_tsumo, is_chankan=is_chankan, is_haitei=is_hotei, ) assert hand_response is not None assert not hand_response.error, hand_response.error cost = hand_response.cost return self.placement.should_call_win(cost, is_tsumo, enemy_seat) def calculate_shanten_or_get_from_cache(self, closed_hand_34: List[int], use_chiitoitsu=True): """ Sometimes we are calculating shanten for the same hand multiple times to save some resources let's cache previous calculations """ key = build_shanten_cache_key(closed_hand_34, use_chiitoitsu) if key in self.hand_cache_shanten: return self.hand_cache_shanten[key] # if use_chiitoitsu and not self.player.is_open_hand: # result = self.shanten_calculator.calculate_shanten_for_chiitoitsu_hand(closed_hand_34) # else: # result = self.shanten_calculator.calculate_shanten_for_regular_hand(closed_hand_34) # fix here: a little bit strange in use_chiitoitsu shanten_results = [] if use_chiitoitsu and not self.player.is_open_hand: shanten_results.append( self.shanten_calculator.calculate_shanten_for_chiitoitsu_hand( closed_hand_34)) shanten_results.append( self.shanten_calculator.calculate_shanten_for_regular_hand( closed_hand_34)) result = min(shanten_results) self.hand_cache_shanten[key] = result return result def calculate_exact_hand_value_or_get_from_cache( self, win_tile_136, tiles=None, call_riichi=False, is_tsumo=False, is_chankan=False, is_haitei=False, is_ippatsu=False, ): if not tiles: tiles = self.player.tiles[:] else: tiles = tiles[:] if win_tile_136 not in tiles: tiles += [win_tile_136] additional_han = 0 if is_chankan: additional_han += 1 if is_haitei: additional_han += 1 if is_ippatsu: additional_han += 1 config = HandConfig( is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind_tile, is_tsumo=is_tsumo, options=OptionalRules( has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao, has_double_yakuman=False, ), is_chankan=is_chankan, is_ippatsu=is_ippatsu, is_haitei=is_tsumo and is_haitei or False, is_houtei=(not is_tsumo) and is_haitei or False, tsumi_number=self.player.table.count_of_honba_sticks, kyoutaku_number=self.player.table.count_of_riichi_sticks, ) return self._estimate_hand_value_or_get_from_cache( win_tile_136, tiles, call_riichi, is_tsumo, additional_han, config) def _estimate_hand_value_or_get_from_cache(self, win_tile_136, tiles, call_riichi, is_tsumo, additional_han, config, is_rinshan=False, is_chankan=False): cache_key = build_estimate_hand_value_cache_key( tiles, call_riichi, is_tsumo, self.player.melds, self.player.table.dora_indicators, self.player.table.count_of_riichi_sticks, self.player.table.count_of_honba_sticks, additional_han, is_rinshan, is_chankan, ) if self.hand_cache_estimation.get(cache_key): return self.hand_cache_estimation.get(cache_key) result = self.finished_hand.estimate_hand_value( tiles, win_tile_136, self.player.melds, self.player.table.dora_indicators, config, use_hand_divider_cache=True, ) self.hand_cache_estimation[cache_key] = result return result @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:] 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: """ pass def get_possible_meld(self, tile, is_kamicha_discard): closed_hand = self.player.closed_hand[:] # we can't open hand anymore if len(closed_hand) == 1: return None, None discarded_tile = tile // 4 closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile]) combinations = [] first_index = 0 second_index = 0 if is_man(discarded_tile): first_index = 0 second_index = 8 elif is_pin(discarded_tile): first_index = 9 second_index = 17 elif is_sou(discarded_tile): first_index = 18 second_index = 26 if second_index == 0: # honor tiles if closed_hand_34[discarded_tile] == 3: combinations = [[[discarded_tile] * 3]] else: # to avoid not necessary calculations # we can check only tiles around +-2 discarded tile first_limit = discarded_tile - 2 if first_limit < first_index: first_limit = first_index second_limit = discarded_tile + 2 if second_limit > second_index: second_limit = second_index combinations = self.hand_divider.find_valid_combinations( closed_hand_34, first_limit, second_limit, True) if combinations: combinations = combinations[0] # possible_melds = [] melds_chi, melds_pon = [], [] for best_meld_34 in combinations: # we can call pon from everyone if is_pon(best_meld_34) and discarded_tile in best_meld_34: if best_meld_34 not in melds_pon: melds_pon.append(best_meld_34) # we can call chi only from left player if is_chi( best_meld_34 ) and is_kamicha_discard and discarded_tile in best_meld_34: if best_meld_34 not in melds_chi: melds_chi.append(best_meld_34) return melds_chi, melds_pon 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: """ pass