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 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 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 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_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_chinitsu(self, hand): """ The hand contains tiles from a single suit :param hand: list of hand's sets :return: true|false """ honor_sets = 0 sou_sets = 0 pin_sets = 0 man_sets = 0 for item in hand: if item[0] in HONOR_INDICES: honor_sets += 1 if is_sou(item[0]): sou_sets += 1 elif is_pin(item[0]): pin_sets += 1 elif is_man(item[0]): man_sets += 1 sets = [sou_sets, pin_sets, man_sets] only_one_suit = len([x for x in sets if x != 0]) == 1 return only_one_suit and honor_sets == 0
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 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, 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 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 rename(self, hand): # rename this yakuman depending on tiles used if is_sou(hand[0][0]): self.set_sou() elif is_pin(hand[0][0]): self.set_pin() else: self.set_man()
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 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_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 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 _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 is_condition_met(self, hand, *args): honor_sets = 0 sou_sets = 0 pin_sets = 0 man_sets = 0 for item in hand: if item[0] in HONOR_INDICES: honor_sets += 1 if is_sou(item[0]): sou_sets += 1 elif is_pin(item[0]): pin_sets += 1 elif is_man(item[0]): man_sets += 1 sets = [sou_sets, pin_sets, man_sets] only_one_suit = len([x for x in sets if x != 0]) == 1 return only_one_suit and honor_sets == 0
def try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): """ Determine should we call a meld or not. If yes, it will return MeldPrint object and tile to discard :param tile: 136 format tile :param is_kamicha_discard: boolean :param new_tiles: :return: MeldPrint and DiscardOption objects """ if self.player.in_riichi: return None, None closed_hand = self.player.closed_hand[:] # we can't open hand anymore if len(closed_hand) == 1: return None, None # we can't use this tile for our chosen strategy if not self.is_tile_suitable(tile): 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.player.ai.hand_divider.find_valid_combinations( closed_hand_34, first_limit, second_limit, True ) if combinations: combinations = combinations[0] possible_melds = [] 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 possible_melds: possible_melds.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 possible_melds: possible_melds.append(best_meld_34) # we can call melds only with allowed tiles validated_melds = [] for meld in possible_melds: if ( self.is_tile_suitable(meld[0] * 4) and self.is_tile_suitable(meld[1] * 4) and self.is_tile_suitable(meld[2] * 4) ): validated_melds.append(meld) possible_melds = validated_melds if not possible_melds: return None, None chosen_meld_dict = self._find_best_meld_to_open(tile, possible_melds, new_tiles, closed_hand, tile) # we didn't find a good discard candidate after open meld if not chosen_meld_dict: return None, None selected_tile = chosen_meld_dict["discard_tile"] meld = chosen_meld_dict["meld"] shanten = selected_tile.shanten had_to_be_called = self.meld_had_to_be_called(tile) had_to_be_called = had_to_be_called or selected_tile.had_to_be_discarded # each strategy can use their own value to min shanten number if shanten > self.min_shanten: self.player.logger.debug( log.MELD_DEBUG, "After meld shanten is too high for our strategy. Abort melding.", ) return None, None # sometimes we had to call tile, even if it will not improve our hand # otherwise we can call only with improvements of shanten if not had_to_be_called and shanten >= self.player.ai.shanten: self.player.logger.debug( log.MELD_DEBUG, "Meld is not improving hand shanten. Abort melding.", ) return None, None if not self.validate_meld(chosen_meld_dict): self.player.logger.debug( log.MELD_DEBUG, "Meld is suitable for strategy logic. Abort melding.", ) return None, None if not self.should_push_against_threats(chosen_meld_dict): self.player.logger.debug( log.MELD_DEBUG, "Meld is too dangerous to call. Abort melding.", ) return None, None return meld, selected_tile
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 try_to_call_meld(self, tile, is_kamicha_discard): """ Determine should we call a meld or not. If yes, it will return Meld object and tile to discard :param tile: 136 format tile :param is_kamicha_discard: boolean :return: Meld and DiscardOption objects """ if self.player.in_riichi: return None, None if self.player.ai.in_defence: return None, None closed_hand = self.player.closed_hand[:] # we can't open hand anymore if len(closed_hand) == 1: return None, None # we can't use this tile for our chosen strategy if not self.is_tile_suitable(tile): return None, None discarded_tile = tile // 4 new_tiles = self.player.tiles[:] + [tile] 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.player.ai.hand_divider.find_valid_combinations(closed_hand_34, first_limit, second_limit, True) if combinations: combinations = combinations[0] possible_melds = [] 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 possible_melds: possible_melds.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 possible_melds: possible_melds.append(best_meld_34) # we can call melds only with allowed tiles validated_melds = [] for meld in possible_melds: if (self.is_tile_suitable(meld[0] * 4) and self.is_tile_suitable(meld[1] * 4) and self.is_tile_suitable(meld[2] * 4)): validated_melds.append(meld) possible_melds = validated_melds if not possible_melds: return None, None best_meld_34 = self._find_best_meld_to_open(possible_melds, new_tiles) if best_meld_34: # we need to calculate count of shanten with supposed meld # to prevent bad hand openings melds = self.player.open_hand_34_tiles + [best_meld_34] outs_results, shanten = self.player.ai.calculate_outs(new_tiles, closed_hand, melds) # each strategy can use their own value to min shanten number if shanten > self.min_shanten: return None, None # we can't improve hand, so we don't need to open it if not outs_results: return None, None # sometimes we had to call tile, even if it will not improve our hand # otherwise we can call only with improvements of shanten if not self.meld_had_to_be_called(tile) and shanten >= self.player.ai.previous_shanten: return None, None meld_type = is_chi(best_meld_34) and Meld.CHI or Meld.PON best_meld_34.remove(discarded_tile) first_tile = TilesConverter.find_34_tile_in_136_array(best_meld_34[0], closed_hand) closed_hand.remove(first_tile) second_tile = TilesConverter.find_34_tile_in_136_array(best_meld_34[1], closed_hand) closed_hand.remove(second_tile) tiles = [ first_tile, second_tile, tile ] meld = Meld() meld.type = meld_type meld.tiles = sorted(tiles) # we had to be sure that all our discard results exists in the closed hand filtered_results = [] for result in outs_results: if result.find_tile_in_hand(closed_hand): filtered_results.append(result) # we can't discard anything, so let's not open our hand if not filtered_results: return None, None selected_tile = self.player.ai.process_discard_options_and_select_tile_to_discard( filtered_results, shanten, had_was_open=True ) return meld, selected_tile return None, None
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 try_to_call_meld(self, tile, is_kamicha_discard, new_tiles): """ Determine should we call a meld or not. If yes, it will return Meld object and tile to discard :param tile: 136 format tile :param is_kamicha_discard: boolean :param new_tiles: :return: Meld and DiscardOption objects """ if self.player.in_riichi: return None, None if self.player.ai.in_defence: return None, None closed_hand = self.player.closed_hand[:] # we can't open hand anymore if len(closed_hand) == 1: return None, None # we can't use this tile for our chosen strategy if not self.is_tile_suitable(tile): 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.player.ai.hand_divider.find_valid_combinations( closed_hand_34, first_limit, second_limit, True ) if combinations: combinations = combinations[0] possible_melds = [] 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 possible_melds: possible_melds.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 possible_melds: possible_melds.append(best_meld_34) # we can call melds only with allowed tiles validated_melds = [] for meld in possible_melds: if (self.is_tile_suitable(meld[0] * 4) and self.is_tile_suitable(meld[1] * 4) and self.is_tile_suitable(meld[2] * 4)): validated_melds.append(meld) possible_melds = validated_melds if not possible_melds: return None, None chosen_meld = self._find_best_meld_to_open(possible_melds, new_tiles, closed_hand, tile) selected_tile = chosen_meld['discard_tile'] meld = chosen_meld['meld'] shanten = selected_tile.shanten had_to_be_called = self.meld_had_to_be_called(tile) had_to_be_called = had_to_be_called or selected_tile.had_to_be_discarded # each strategy can use their own value to min shanten number if shanten > self.min_shanten: return None, None # sometimes we had to call tile, even if it will not improve our hand # otherwise we can call only with improvements of shanten if not had_to_be_called and shanten >= self.player.ai.shanten: return None, None return meld, selected_tile
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
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 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 find_suji(self, tiles_136): tiles_34 = list(set([x // 4 for x in tiles_136])) suji = [] suits = [[], [], []] # let's cast each tile to 0-8 presentation for tile in 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) all_suji = list(set(suji)) result = [] for suji in all_suji: suji_temp = suji % 9 base = suji - suji_temp - 1 if suji_temp == self.FIRST_SUJI: result += [base + 1, base + 4, base + 7] if suji_temp == self.SECOND_SUJI: result += [base + 2, base + 5, base + 8] if suji_temp == self.THIRD_SUJI: result += [base + 3, base + 6, base + 9] return result