async def read_item(winning_tile: str, man_tile: str = None, pin_tile: str = None, sou_tile: str = None): calculator = HandCalculator() # アガリ形(man=マンズ, pin=ピンズ, sou=ソーズ, honors=字牌) tiles = TilesConverter.string_to_136_array(man=man_tile, pin=pin_tile, sou=sou_tile) # アガリ牌(ソーズの5) win_tile = TilesConverter.string_to_136_array(sou=winning_tile)[0] # 鳴き(チー:CHI, ポン:PON, カン:KAN(True:ミンカン,False:アンカン), カカン:CHANKAN, ヌキドラ:NUKI) melds = None # ドラ(なし) dora_indicators = None # オプション(なし) config = HandConfig(is_tsumo=True) result = calculator.estimate_hand_value(tiles, win_tile, melds, dora_indicators, config) return { "result_main": result.cost['main'], "result_additional": result.cost['additional'], "yaku": result.yaku }
def find_tile_in_hand(self, closed_hand): """ Find and return 136 tile in closed player hand """ if self.player.table.has_aka_dora: tiles_five_of_suits = [4, 13, 22] # special case, to keep aka dora in hand if self.tile_to_discard in tiles_five_of_suits: aka_closed_hand = closed_hand[:] while True: tile = TilesConverter.find_34_tile_in_136_array(self.tile_to_discard, aka_closed_hand) # we have only aka dora in the hand, without simple five if not tile: break # we found aka in the hand, # let's try to search another five tile # to keep aka dora if tile in AKA_DORA_LIST: aka_closed_hand.remove(tile) else: return tile return TilesConverter.find_34_tile_in_136_array(self.tile_to_discard, closed_hand)
def find_tile_in_hand(self, closed_hand): """ Find and return 136 tile in closed player hand """ if settings.FIVE_REDS: # special case, to keep aka dora in hand if self.tile_to_discard in [4, 13, 22]: aka_closed_hand = closed_hand[:] while True: tile = TilesConverter.find_34_tile_in_136_array( self.tile_to_discard, aka_closed_hand) # we have only aka dora in the hand if not tile: break # we found aka in the hand, # let's try to search another five tile # to keep aka dora if tile in AKA_DORA_LIST: aka_closed_hand.remove(tile) else: return tile return TilesConverter.find_34_tile_in_136_array( self.tile_to_discard, closed_hand)
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
def hand_calculator(tiles, win_tile, config=HandConfig()): calculator = HandCalculator() tiles = TilesConverter.one_line_string_to_136_array(str(tiles), has_aka_dora=True) win_tile = TilesConverter.one_line_string_to_136_array( str(win_tile), has_aka_dora=True)[0] return calculator.estimate_hand_value(tiles, win_tile, config=config)
def check_pinfu(man, pin, sou, honors, player_wind, round_wind, win_tile_type, win_tile_value): calculator = HandCalculator() tiles = TilesConverter.string_to_136_array(man=man, pin=pin, sou=sou, honors=honors) print(tiles) win_tile = TilesConverter.string_to_136_array( **{win_tile_type: win_tile_value})[0] config = HandConfig(player_wind=player_wind, round_wind=round_wind) result = calculator.estimate_hand_value(tiles, win_tile, config=config) if result.yaku is not None: for yaku in result.yaku: if yaku.name == "Pinfu": cost = 1500 if config.is_dealer else 1000 return [ json.dumps({ 'isPinfu': True, 'cost': cost }).encode("utf-8") ] return [json.dumps({'isPinfu': False, 'cost': 0}).encode("utf-8")]
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 find_tile_in_hand(self, closed_hand): """ Find and return 136 tile in closed player hand """ if self.player.table.has_aka_dora: tiles_five_of_suits = [4, 13, 22] # special case, to keep aka dora in hand if self.tile_to_discard in tiles_five_of_suits: aka_closed_hand = closed_hand[:] while True: tile = TilesConverter.find_34_tile_in_136_array( self.tile_to_discard, aka_closed_hand) # we have only aka dora in the hand, without simple five if not tile: break # we found aka in the hand, # let's try to search another five tile # to keep aka dora if tile in AKA_DORA_LIST: aka_closed_hand.remove(tile) else: return tile return TilesConverter.find_34_tile_in_136_array( self.tile_to_discard, closed_hand)
def discard_tile(self, discard_tile=None): """ :param discard_tile: 136 tile format :return: """ tile_to_discard = self.ai.discard_tile(discard_tile) is_tsumogiri = tile_to_discard == self.last_draw # it is important to use table method, # to recalculate revealed tiles and etc. self.table.add_discarded_tile(0, tile_to_discard, is_tsumogiri) # debug defence with honors if tile_to_discard in self.tiles: self.tiles.remove(tile_to_discard) else: logger.info("Catch that bug: defence with honors") logger.info("Hand: {}\nTile: {}".format( TilesConverter.to_one_line_string(self.tiles), TilesConverter.to_one_line_string([tile_to_discard]))) logger.info("Hand: {}\nTile: {}".format(self.tiles, tile_to_discard)) return self.tiles[-1] return tile_to_discard
def test_one_line_string_to_136_array(self): initial_string = '789m456p555s11222z' tiles = TilesConverter.one_line_string_to_136_array(initial_string) self.assertEqual(len(tiles), 14) new_string = TilesConverter.to_one_line_string(tiles) self.assertEqual(initial_string, new_string)
def discard_tile(self): h = Hand(TilesConverter.to_one_line_string(self.player.tiles)) tiles = TilesConverter.to_34_array(self.player.tiles) shanten = self.shanten.calculate_shanten(tiles) if shanten == 0: self.player.in_tempai = True if h.test_win(): return Shanten.AGARI_STATE elif self.player.in_tempai: results, st = self.calculate_outs() tile34 = results[0]['discard'] tile_in_hand = TilesConverter.find_34_tile_in_136_array( tile34, self.player.tiles) return tile_in_hand else: hand_data = h.get_data() it = int( self.model.predict_classes(transformCSVHandToCNNMatrix( expandHandToCSV(hand_data)), verbose=0)[0]) t = hand_data[it] tile_in_hand = self.mahjong_tile_to_discard_tile(t) return tile_in_hand
def translateIntoMsg(res: dict) -> str: Msg = {} finalStr = '' if -1 in res.keys(): arr = res[-1] res.pop(-1) finalStr += ' 摸 ' for i in arr: finalStr += '%s' % TilesConverter.to_one_line_string([i]) if -2 in res.keys(): finalStr += '\n %s' % res[-2] return finalStr for k in res: name = TilesConverter.to_one_line_string([k]) tiles = TilesConverter.to_34_array(res[k]) for i in range(0, len(tiles)): tiles[i] = 1 if tiles[i] > 0 else 0 tiles = TilesConverter.to_136_array(tiles) chance = '' for i in tiles: chance += TilesConverter.to_one_line_string([i]) if len(res[k]) in Msg.keys(): Msg[len(res[k])].append('打%s 摸[' % name + chance + ' %d枚]' % len(res[k])) else: Msg[len( res[k])] = ['打%s 摸[' % name + chance + ' %d枚]' % len(res[k])] keys = sorted(Msg, reverse=True) for k in keys: for item in Msg[k]: finalStr += '\n' + item return finalStr
def calculate_outs(self): tiles = TilesConverter.to_34_array(self.player.tiles) shanten = self.shanten.calculate_shanten(tiles) # win if shanten == Shanten.AGARI_STATE: return [], shanten raw_data = {} for i in range(0, 34): if not tiles[i]: continue tiles[i] -= 1 raw_data[i] = [] for j in range(0, 34): if i == j or tiles[j] >= 4: continue tiles[j] += 1 if self.shanten.calculate_shanten(tiles) == shanten - 1: raw_data[i].append(j) tiles[j] -= 1 tiles[i] += 1 if raw_data[i]: raw_data[i] = { 'tile': i, 'tiles_count': self.count_tiles(raw_data[i], tiles), 'waiting': raw_data[i] } results = [] tiles = TilesConverter.to_34_array(self.player.tiles) for tile in range(0, len(tiles)): if tile in raw_data and raw_data[tile] and raw_data[tile][ 'tiles_count']: item = raw_data[tile] waiting = [] for item2 in item['waiting']: waiting.append(item2) results.append({ 'discard': item['tile'], 'waiting': waiting, 'tiles_count': item['tiles_count'] }) # if we have character and honor candidates to discard with same tiles count, # we need to discard honor tile first results = sorted(results, key=lambda x: (x['tiles_count'], x['discard']), reverse=True) return results, shanten
def test_find_34_tile_in_136_array(self): result = TilesConverter.find_34_tile_in_136_array(0, [3, 4, 5, 6]) self.assertEqual(result, 3) result = TilesConverter.find_34_tile_in_136_array(33, [3, 4, 134, 135]) self.assertEqual(result, 134) result = TilesConverter.find_34_tile_in_136_array(20, [3, 4, 134, 135]) self.assertEqual(result, None)
def find_discard_options(self): """ :param tiles: array of tiles in 136 format :param closed_hand: array of tiles in 136 format :return: """ self._assert_hand_correctness() tiles = self.player.tiles closed_hand = self.player.closed_hand tiles_34 = TilesConverter.to_34_array(tiles) closed_tiles_34 = TilesConverter.to_34_array(closed_hand) is_agari = self.ai.agari.is_agari(tiles_34, self.player.meld_34_tiles) # we decide beforehand if we need to consider chiitoitsu for all of our possible discards min_shanten, use_chiitoitsu = self.calculate_shanten_and_decide_hand_structure( closed_tiles_34) results = [] tile_34_prev = None # we iterate in reverse order to naturally handle aka-doras, i.e. discard regular 5 if we have it for tile_136 in reversed(self.player.closed_hand): tile_34 = tile_136 // 4 # already added if tile_34 == tile_34_prev: continue else: tile_34_prev = tile_34 closed_tiles_34[tile_34] -= 1 waiting, shanten = self.calculate_waits( closed_tiles_34, tiles_34, use_chiitoitsu=use_chiitoitsu) assert shanten >= min_shanten closed_tiles_34[tile_34] += 1 if waiting: wait_to_ukeire = dict( zip(waiting, [ self.count_tiles([x], closed_tiles_34) for x in waiting ])) results.append( DiscardOption( player=self.player, shanten=shanten, tile_to_discard_136=tile_136, waiting=waiting, ukeire=sum(wait_to_ukeire.values()), wait_to_ukeire=wait_to_ukeire, )) if is_agari: shanten = Shanten.AGARI_STATE else: shanten = min_shanten return results, shanten
def to_34_array(obj=None, man=None, pin=None, sou=None, honors=None): if obj == None: return TilesConverter.string_to_34_array(man=man, pin=pin, sou=sou, honors=honors) else: return TilesConverter.string_to_34_array(man=obj["man"], pin=obj["pin"], sou=obj["sou"])
def __init__(self, tiles, dora_indicators, revealed_tiles): self.tiles = tiles self.Shanten_calculater = Shanten() self.Hand_Calculator = HandCalculator() self.Tiles = TilesConverter() self.shanten = self.Shanten_calculater.calculate_shanten( self.Tiles.to_34_array(self.tiles)) self.Hand_Config = HandConfig(is_riichi=True) self.dora_indicators = dora_indicators self.revealed_tiles = revealed_tiles
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 tiles_34[tile] -= 1 h = sum( self.simulate(tiles_34, self.player.open_hand_34_tiles, unaccounted) for _ in range(200)) tiles_34[tile] += 1 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
def getTile(s: str) -> dict: #translate s into supported format if len(s) == 0: return {} tiles = TilesConverter.one_line_string_to_136_array(s, True) test = {} # check tiles length added = [] if len(tiles) % 3 == 0: #3n, add a random tile while True: tmp = randint(0, 135) if tmp not in tiles: tiles.append(tmp) added.append(tmp) break if len(tiles) % 3 == 1: #3n+1, add a random tile while True: tmp = randint(0, 135) if tmp not in tiles: tiles.append(tmp) added.append(tmp) break if len(added) > 0: test[-1] = added # now is a normal form tiles.sort() avaliable = [] for i in range(0, 136): if i not in tiles: avaliable.append(i) calculator = Shanten() baseShanten = calculator.calculate_shanten( TilesConverter.to_34_array(tiles)) if baseShanten == -1: return {-2: '已和牌'} #14*122 try tmp = copy.deepcopy(tiles) for t in tiles: tmp.remove(t) one_try = [] for i in avaliable: tmp.append(i) res = calculator.calculate_shanten( TilesConverter.to_34_array(sorted(tmp))) if res < baseShanten: one_try.append(i) tmp.remove(i) t = 4 * (t // 4) if len(one_try) > 0 and t not in test.keys(): test[t] = copy.deepcopy(one_try) tmp.append(t) return test
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 tiles_34[tile] -= 1 h = sum( self.simulate(tiles_34, open_hand_34, unaccounted) for _ in range(200)) tiles_34[tile] += 1 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
def format_hand_for_print(self, tile): hand_string = '{} + {}'.format( TilesConverter.to_one_line_string(self.closed_hand), TilesConverter.to_one_line_string([tile])) if self.is_open_hand: melds = [] for item in self.melds: melds.append('{}'.format( TilesConverter.to_one_line_string(item.tiles))) hand_string += ' [{}]'.format(', '.join(melds)) return hand_string
def set_state(self, play_state): logger.info("Set the player's state from {} to {}".format( self.play_state, play_state)) logger.info("Hand: {}".format( TilesConverter.to_one_line_string(self.closed_hand))) logger.info("Outs: {} {}".format( TilesConverter.to_one_line_string( [out * 4 for out in self.ai.waiting]), self.ai.wanted_tiles_count)) self.play_state = play_state self.latest_change_index = len(self.discards)
def _format_hand_for_print(self, tiles, new_tile, melds): tiles_string = TilesConverter.to_one_line_string(tiles, print_aka_dora=self.player.table.has_aka_dora) tile_string = TilesConverter.to_one_line_string([new_tile], print_aka_dora=self.player.table.has_aka_dora) hand_string = f"{tiles_string} + {tile_string}" hand_string += " [{}]".format( ", ".join( [ TilesConverter.to_one_line_string(x.tiles, print_aka_dora=self.player.table.has_aka_dora) for x in melds ] ) ) return hand_string
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 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 format_hand_for_print(self, tile_136=None): hand_string = '{}'.format(TilesConverter.to_one_line_string(self.closed_hand)) if tile_136 is not None: hand_string += ' + {}'.format(TilesConverter.to_one_line_string([tile_136])) melds = [] for item in self.melds: melds.append('{}'.format(TilesConverter.to_one_line_string(item.tiles))) if melds: hand_string += ' [{}]'.format(', '.join(melds)) return hand_string
def main(): hand = random.choice(questions) round_wind, player_wind = get_random_kaze_set() tiles = TilesConverter.string_to_136_array( man=hand.get_man(), pin=hand.get_pin(), sou=hand.get_sou()) win_tile = TilesConverter.string_to_136_array( **hand.get_win_tile())[0] conf = { 'is_tsumo': get_is_or_not(), 'is_riichi': get_is_or_not(), 'player_wind': player_wind, 'round_wind': round_wind } config = HandConfig(**conf) result = calculator.estimate_hand_value(tiles, win_tile, config=config) question_caption = '\n' if config.is_tsumo: question_caption += f"ツモ:{hand.get_win_tile_figure()} " else: question_caption += f"ロン:{hand.get_win_tile_figure()} " if config.is_riichi: question_caption += 'リーチ有 ' else: question_caption += 'リーチ無 ' question_caption += f"場風: {DISPLAY_WINDS_JP[config.round_wind]} 自風: {DISPLAY_WINDS_JP[config.player_wind]}" print(hand.get_figure()) print(question_caption) if config.is_tsumo and config.player_wind == EAST: child_answer = int(input('子の支払う点数: ')) if child_answer == result.cost['main']: print('正解!!') else: print(f"不正解!! 正解は {result.cost['main']} オール") elif config.is_tsumo and config.player_wind != EAST: parent_answer = int(input('親の支払う点数: ')) child_answer = int(input('子の支払う点数: ')) if parent_answer == result.cost['main'] and child_answer == result.cost['additional']: print('正解!!') else: print( f"不正解!! 正解は 親: {result.cost['main']}, 子: {result.cost['additional']}") else: answer = int(input('放銃者の支払う点数: ')) if answer == result.cost['main']: print('正解!!') else: print(f"不正解!! 正解は {result.cost['main']}")
def calculate_outs(self, tiles, closed_hand, open_sets_34=None): """ :param tiles: array of tiles in 136 format :param closed_hand: array of tiles in 136 format :param open_sets_34: array of array with tiles in 34 format :return: """ tiles_34 = TilesConverter.to_34_array(tiles) closed_tiles_34 = TilesConverter.to_34_array(closed_hand) is_agari = self.agari.is_agari(tiles_34, self.player.open_hand_34_tiles) results = [] for hand_tile in range(0, 34): if not closed_tiles_34[hand_tile]: continue tiles_34[hand_tile] -= 1 shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) waiting = [] for j in range(0, 34): if hand_tile == j or tiles_34[j] == 4: continue tiles_34[j] += 1 if self.shanten.calculate_shanten(tiles_34, open_sets_34) == shanten - 1: waiting.append(j) tiles_34[j] -= 1 tiles_34[hand_tile] += 1 if waiting: results.append( DiscardOption(player=self.player, shanten=shanten, tile_to_discard=hand_tile, waiting=waiting, tiles_count=self.count_tiles( waiting, tiles_34))) if is_agari: shanten = Shanten.AGARI_STATE else: shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) return results, shanten
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 draw_tile(self, tile): """ :param tile: 136 tile format :return: """ self.player.in_tempai = False self.shanten_calculator = Shanten() Tile = TilesConverter() current_shanten = self.shanten_calculator.calculate_shanten( Tile.to_34_array(self.player.tiles)) logger.debug('Current_shanten:{}'.format(current_shanten)) if current_shanten == 0: self.player.in_tempai = True logger.debug('whether_in_tempai:{}'.format(self.player.in_tempai)) self.shanten_calculator = None
def is_threatening(self): """ Should we fold against this player or not :return: boolean """ if self.player.in_riichi: return True discards = self.player.discards discards_34 = TilesConverter.to_34_array([x.value for x in discards]) is_honitsu_open_sets, open_hand_suit = False, None is_honitsu_discards, discard_suit = self._is_honitsu_discards( discards_34) meld_tiles = self.player.meld_tiles meld_tiles_34 = TilesConverter.to_34_array(meld_tiles) if meld_tiles: dora_count = sum( [plus_dora(x, self.table.dora_indicators) for x in meld_tiles]) # aka dora dora_count += sum([ 1 for x in meld_tiles if is_aka_dora(x, self.table.has_open_tanyao) ]) # enemy has a lot of dora tiles in his opened sets # so better to fold against him if dora_count >= 3: return True # check that user has a discard and melds that looks like honitsu is_honitsu_open_sets, open_hand_suit = self._is_honitsu_open_sets( meld_tiles_34) if is_honitsu_open_sets: # for 2 opened melds we had to check discard, to be sure if len( self.player.melds ) <= 2 and is_honitsu_discards and discard_suit == open_hand_suit: self.chosen_suit = open_hand_suit return True # for 3+ opened melds there is no sense to check discard if len(self.player.melds) >= 3: self.chosen_suit = open_hand_suit return True return False
def serialize(self): data = { "tile": TilesConverter.to_one_line_string( [self.tile_to_discard_136], print_aka_dora=self.player.table.has_aka_dora), "shanten": self.shanten, "ukeire": self.ukeire, "valuation": self.valuation, "danger": { "max_danger": self.danger.get_max_danger(), "sum_danger": self.danger.get_sum_danger(), "weighted_danger": self.danger.get_weighted_danger(), "min_border": self.danger.get_min_danger_border(), "danger_border": self.danger.danger_border, "weighted_cost": self.danger.weighted_cost, "danger_reasons": self.danger.values, "can_be_used_for_ryanmen": self.danger.can_be_used_for_ryanmen, }, } if self.ukeire_second: data["ukeire2"] = self.ukeire_second if self.average_second_level_waits: data[ "average_second_level_waits"] = self.average_second_level_waits if self.average_second_level_cost: data["average_second_level_cost"] = self.average_second_level_cost if self.had_to_be_saved: data["had_to_be_saved"] = self.had_to_be_saved if self.had_to_be_discarded: data["had_to_be_discarded"] = self.had_to_be_discarded return data
def should_call_riichi(self): # empty waiting can be found in some cases if not self.player.ai.waiting: return False if self.player.ai.in_defence: return False # don't call karaten riichi count_tiles = self.player.ai.hand_builder.count_tiles( self.player.ai.waiting, TilesConverter.to_34_array(self.player.closed_hand) ) if count_tiles == 0: return False # It is daburi! first_discard = self.player.round_step == 1 if first_discard and not self.player.table.meld_was_called: return True if len(self.player.ai.waiting) == 1: return self._should_call_riichi_one_sided() return self._should_call_riichi_many_sided()
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 meld_had_to_be_called(self, tile): tile //= 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2] # for big shanten number we don't need to check already opened pon set, # because it will improve our hand anyway if self.player.ai.shanten < 2: for meld in self.player.melds: # we have already opened yakuhai pon # so we don't need to open hand without shanten improvement if self._is_yakuhai_pon(meld): return False # if we don't have any yakuhai pon and this is our last chance, we must call this tile if tile in self.last_chance_calls: return True # in all other cases for closed hand we don't need to open hand with special conditions if not self.player.is_open_hand: return False # we have opened the hand already and don't yet have yakuhai pon # so we now must get it for valued_pair in valued_pairs: if valued_pair == tile: return True return False
def should_activate_strategy(self, tiles_136): """ We can go for chiitoitsu strategy if we have 5 pairs """ result = super(ChiitoitsuStrategy, self).should_activate_strategy(tiles_136) if not result: return False tiles_34 = TilesConverter.to_34_array(self.player.tiles) num_pairs = len([x for x in range(0, 34) if tiles_34[x] == 2]) num_pons = len([x for x in range(0, 34) if tiles_34[x] == 3]) # for now we don't consider chiitoitsu with less than 5 pair if num_pairs < 5: return False # if we have 5 pairs and tempai, this is obviously not chiitoitsu if num_pairs == 5 and self.player.ai.shanten == 0: return False # for now we won't go for chiitoitsu if we have 5 pairs and pon if num_pairs == 5 and num_pons > 0: return False return True
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 __unicode__(self): tile_format_136 = TilesConverter.to_one_line_string([self.tile_to_discard*4]) return 'tile={}, shanten={}, ukeire={}, ukeire2={}, valuation={}'.format( tile_format_136, self.shanten, self.ukeire, self.ukeire_second, self.valuation )
def find_discard_options(self, tiles, closed_hand, melds=None): """ :param tiles: array of tiles in 136 format :param closed_hand: array of tiles in 136 format :param melds: :return: """ if melds is None: melds = [] open_sets_34 = [x.tiles_34 for x in melds] tiles_34 = TilesConverter.to_34_array(tiles) closed_tiles_34 = TilesConverter.to_34_array(closed_hand) is_agari = self.ai.agari.is_agari(tiles_34, self.player.meld_34_tiles) results = [] for hand_tile in range(0, 34): if not closed_tiles_34[hand_tile]: continue tiles_34[hand_tile] -= 1 waiting, shanten = self.calculate_waits(tiles_34, open_sets_34) tiles_34[hand_tile] += 1 if waiting: wait_to_ukeire = dict(zip(waiting, [self.count_tiles([x], closed_tiles_34) for x in waiting])) results.append(DiscardOption(player=self.player, shanten=shanten, tile_to_discard=hand_tile, waiting=waiting, ukeire=self.count_tiles(waiting, closed_tiles_34), wait_to_ukeire=wait_to_ukeire)) if is_agari: shanten = Shanten.AGARI_STATE else: shanten, _ = self.calculate_shanten( tiles_34, open_sets_34 ) return results, shanten
def divide_hand(self, tiles, waiting): tiles_copy = tiles.copy() for i in range(0, 4): if waiting * 4 + i not in tiles_copy: tiles_copy += [waiting * 4 + i] break tiles_34 = TilesConverter.to_34_array(tiles_copy) results = self.player.ai.hand_divider.divide_hand(tiles_34) return results, tiles_34
def _should_call_riichi_many_sided(self): count_tiles = self.player.ai.hand_builder.count_tiles( self.player.ai.waiting, TilesConverter.to_34_array(self.player.closed_hand) ) hand_costs = [] waits_with_yaku = 0 for waiting in self.player.ai.waiting: hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False) if hand_value.error is None: hand_costs.append(hand_value.cost['main']) if hand_value.yaku is not None and hand_value.cost is not None: waits_with_yaku += 1 # if we have yaku on every wait if waits_with_yaku == len(self.player.ai.waiting): min_cost = min(hand_costs) # let's not riichi this bad wait if count_tiles <= 2: return False # if wait is slighly better, we will riichi only a cheap hand if count_tiles <= 4: if self.player.is_dealer and min_cost >= 7700: return False if not self.player.is_dealer and min_cost >= 5200: return False return True # wait is even better, but still don't call riichi on damaten mangan if count_tiles <= 6: if self.player.is_dealer and min_cost >= 11600: return False if not self.player.is_dealer and min_cost >= 7700: return False return True # if wait is good we only damaten haneman if self.player.is_dealer and min_cost >= 18000: return False if not self.player.is_dealer and min_cost >= 12000: return False return True # if we don't have yaku on every wait and it's two-sided or more, we call riichi return True
def is_threatening(self): """ Should we fold against this player or not :return: boolean """ if self.player.in_riichi: return True discards = self.player.discards discards_34 = TilesConverter.to_34_array([x.value for x in discards]) is_honitsu_open_sets, open_hand_suit = False, None is_honitsu_discards, discard_suit = self._is_honitsu_discards(discards_34) meld_tiles = self.player.meld_tiles meld_tiles_34 = TilesConverter.to_34_array(meld_tiles) if meld_tiles: dora_count = sum([plus_dora(x, self.table.dora_indicators) for x in meld_tiles]) # aka dora dora_count += sum([1 for x in meld_tiles if is_aka_dora(x, self.table.has_open_tanyao)]) # enemy has a lot of dora tiles in his opened sets # so better to fold against him if dora_count >= 3: return True # check that user has a discard and melds that looks like honitsu is_honitsu_open_sets, open_hand_suit = self._is_honitsu_open_sets(meld_tiles_34) if is_honitsu_open_sets: # for 2 opened melds we had to check discard, to be sure if len(self.player.melds) <= 2 and is_honitsu_discards and discard_suit == open_hand_suit: self.chosen_suit = open_hand_suit return True # for 3+ opened melds there is no sense to check discard if len(self.player.melds) >= 3: self.chosen_suit = open_hand_suit return True return False
def __str__(self): dora_string = TilesConverter.to_one_line_string(self.dora_indicators) round_settings = { EAST: ['e', 0], SOUTH: ['s', 3], WEST: ['w', 7] }.get(self.round_wind_tile) round_string, round_diff = round_settings display_round = '{}{}'.format(round_string, (self.round_wind_number + 1) - round_diff) return 'Round: {}, Honba: {}, Dora Indicators: {}'.format( display_round, self.count_of_honba_sticks, dora_string )
def determine_what_to_discard(self, discard_options, hand, open_melds): is_open_hand = len(open_melds) > 0 tiles_34 = TilesConverter.to_34_array(hand) valued_pairs = [x for x in self.player.valued_honors if tiles_34[x] == 2] # closed pon sets valued_pons = [x for x in self.player.valued_honors if tiles_34[x] == 3] # open pon sets valued_pons += [x for x in open_melds if x.type == Meld.PON and x.tiles[0] // 4 in self.player.valued_honors] acceptable_options = [] for item in discard_options: if is_open_hand: if len(valued_pons) == 0: # don't destroy our only yakuhai pair if len(valued_pairs) == 1 and item.tile_to_discard in valued_pairs: continue elif len(valued_pons) == 1: # don't destroy our only yakuhai pon if item.tile_to_discard in valued_pons: continue acceptable_options.append(item) # we don't have a choice if not acceptable_options: return discard_options preferred_options = [] for item in acceptable_options: # ignore wait without yakuhai yaku if possible if is_open_hand and len(valued_pons) == 0 and len(valued_pairs) == 1: if item.shanten == 0: if valued_pairs[0] not in item.waiting: continue preferred_options.append(item) if not preferred_options: return acceptable_options return preferred_options
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 _choose_best_discard_in_tempai(self, tiles, melds, discard_options): # first of all we find tiles that have the best hand cost * ukeire value call_riichi = not self.player.is_open_hand discard_desc = [] player_tiles_copy = self.player.tiles.copy() player_melds_copy = self.player.melds.copy() closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) for discard_option in discard_options: tile = discard_option.find_tile_in_hand(self.player.closed_hand) # temporary remove discard option to estimate hand value self.player.tiles = tiles.copy() self.player.tiles.remove(tile) # temporary replace melds self.player.melds = melds.copy() # for kabe/suji handling discarded_tile = Tile(tile, False) self.player.discards.append(discarded_tile) is_furiten = self._is_discard_option_furiten(discard_option) if len(discard_option.waiting) == 1: waiting = discard_option.waiting[0] cost_x_ukeire, hand_cost = self._estimate_cost_x_ukeire(discard_option, call_riichi) # let's check if this is a tanki wait results, tiles_34 = self.divide_hand(self.player.tiles, waiting) result = results[0] tanki_type = None is_tanki = False for hand_set in result: if waiting not in hand_set: continue if is_pair(hand_set): is_tanki = True if is_honor(waiting): # TODO: differentiate between self honor and honor for all players if waiting in self.player.valued_honors: tanki_type = self.TankiWait.TANKI_WAIT_ALL_YAKUHAI else: tanki_type = self.TankiWait.TANKI_WAIT_NON_YAKUHAI break simplified_waiting = simplify(waiting) have_suji, have_kabe = self.check_suji_and_kabe(closed_tiles_34, waiting) # TODO: not sure about suji/kabe priority, so we keep them same for now if 3 <= simplified_waiting <= 5: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_456_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_456_RAW elif 2 <= simplified_waiting <= 6: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_37_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_37_RAW elif 1 <= simplified_waiting <= 7: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_28_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_28_RAW else: if have_suji or have_kabe: tanki_type = self.TankiWait.TANKI_WAIT_69_KABE else: tanki_type = self.TankiWait.TANKI_WAIT_69_RAW break discard_desc.append({ 'discard_option': discard_option, 'hand_cost': hand_cost, 'cost_x_ukeire': cost_x_ukeire, 'is_furiten': is_furiten, 'is_tanki': is_tanki, 'tanki_type': tanki_type }) else: cost_x_ukeire, _ = self._estimate_cost_x_ukeire(discard_option, call_riichi) discard_desc.append({ 'discard_option': discard_option, 'hand_cost': None, 'cost_x_ukeire': cost_x_ukeire, 'is_furiten': is_furiten, 'is_tanki': False, 'tanki_type': None }) # reverse all temporary tile tweaks self.player.tiles = player_tiles_copy self.player.melds = player_melds_copy self.player.discards.remove(discarded_tile) discard_desc = sorted(discard_desc, key=lambda k: (k['cost_x_ukeire'], not k['is_furiten']), reverse=True) # if we don't have any good options, e.g. all our possible waits ara karaten # FIXME: in that case, discard the safest tile if discard_desc[0]['cost_x_ukeire'] == 0: return sorted(discard_options, key=lambda x: x.valuation)[0] num_tanki_waits = len([x for x in discard_desc if x['is_tanki']]) # what if all our waits are tanki waits? we need a special handling for that case if num_tanki_waits == len(discard_options): return self._choose_best_tanki_wait(discard_desc) best_discard_desc = [x for x in discard_desc if x['cost_x_ukeire'] == discard_desc[0]['cost_x_ukeire']] # we only have one best option based on ukeire and cost, nothing more to do here if len(best_discard_desc) == 1: return best_discard_desc[0]['discard_option'] # if we have several options that give us similar wait # FIXME: 1. we find the safest tile to discard # FIXME: 2. if safeness is the same, we try to discard non-dora tiles return best_discard_desc[0]['discard_option']
def should_activate_strategy(self, tiles_136): """ We can go for yakuhai strategy if we have at least one yakuhai pair in the hand :return: boolean """ result = super(YakuhaiStrategy, self).should_activate_strategy(tiles_136) if not result: return False tiles_34 = TilesConverter.to_34_array(tiles_136) player_hand_tiles_34 = TilesConverter.to_34_array(self.player.tiles) player_closed_hand_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) self.valued_pairs = [x for x in self.player.valued_honors if player_hand_tiles_34[x] == 2] is_double_east_wind = len([x for x in self.valued_pairs if x == EAST]) == 2 is_double_south_wind = len([x for x in self.valued_pairs if x == SOUTH]) == 2 self.valued_pairs = list(set(self.valued_pairs)) self.has_valued_pon = len([x for x in self.player.valued_honors if player_hand_tiles_34[x] >= 3]) >= 1 opportunity_to_meld_yakuhai = False for x in range(0, 34): if x in self.valued_pairs and tiles_34[x] - player_hand_tiles_34[x] == 1: opportunity_to_meld_yakuhai = True has_valued_pair = False for pair in self.valued_pairs: # we have valued pair in the hand and there are enough tiles # in the wall if opportunity_to_meld_yakuhai or self.player.total_tiles(pair, player_closed_hand_tiles_34) < 4: has_valued_pair = True break # we don't have valuable pair or pon to open our hand if not has_valued_pair and not self.has_valued_pon: return False # let's always open double east if is_double_east_wind: return True # let's open double south if we have a dora in the hand # or we have other valuable pairs if is_double_south_wind and (self.dora_count_total >= 1 or len(self.valued_pairs) >= 2): return True # If we have 1+ dora in the hand and there are 2+ valuable pairs let's open hand if len(self.valued_pairs) >= 2 and self.dora_count_total >= 1: return True # If we have 2+ dora in the hand let's open hand if self.dora_count_total >= 2: for x in range(0, 34): # we have other pair in the hand # so we can open hand for atodzuke if player_hand_tiles_34[x] >= 2 and x not in self.valued_pairs: self.go_for_atodzuke = True return True # If we have 1+ dora in the hand and there is 5+ round step let's open hand if self.dora_count_total >= 1 and self.player.round_step > 5: return True for pair in self.valued_pairs: # last chance to get that yakuhai, let's go for it if (opportunity_to_meld_yakuhai and self.player.total_tiles(pair, player_closed_hand_tiles_34) == 3 and self.player.ai.shanten >= 1): if pair not in self.last_chance_calls: self.last_chance_calls.append(pair) return True return False
def should_activate_strategy(self, tiles_136): """ We can go for honitsu strategy if we have prevalence of one suit and honor tiles """ result = super(HonitsuStrategy, self).should_activate_strategy(tiles_136) if not result: return False tiles_34 = TilesConverter.to_34_array(tiles_136) suits = count_tiles_by_suits(tiles_34) suits = [x for x in suits if x['name'] != 'honor'] suits = sorted(suits, key=lambda x: x['count'], reverse=True) suit = suits[0] count_of_shuntsu_other_suits = 0 count_of_koutsu_other_suits = 0 count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[1]['function']) count_of_shuntsu_other_suits += self._count_of_shuntsu(tiles_34, suits[2]['function']) count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[1]['function']) count_of_koutsu_other_suits += self._count_of_koutsu(tiles_34, suits[2]['function']) self._calculate_not_suitable_tiles_cnt(tiles_34, suit['function']) self._initialize_honitsu_dora_count(tiles_136, suit) # let's not go for honitsu if we have 5 or more non-isolated # tiles in other suits if self.tiles_count_other_suits >= 5: return False # let's not go for honitsu if we have 2 or more non-isolated doras # in other suits if self.dora_count_other_suits_not_isolated >= 2: return False # if we have a pon of valued doras, let's not go for honitsu # we have a mangan anyway, let's go for fastest hand valued_pons = [x for x in self.player.valued_honors if tiles_34[x] >= 3] for pon in valued_pons: dora_count = plus_dora(pon * 4, self.player.table.dora_indicators) if dora_count > 0: return False valued_pairs = len([x for x in self.player.valued_honors if tiles_34[x] == 2]) honor_pairs_or_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2]) honor_doras_pairs_or_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 2 and plus_dora(x * 4, self.player.table.dora_indicators)]) unvalued_singles = len([x for x in range(0, 34) if is_honor(x) and x not in self.player.valued_honors and tiles_34[x] == 1]) # if we have some decent amount of not isolated tiles in other suits # we may not rush for honitsu considering other conditions if self.tiles_count_other_suits_not_isolated >= 3: # if we don't have pair or pon of honored doras if honor_doras_pairs_or_pons == 0: # we need to either have a valued pair or have at least two honor # pairs to consider honitsu if valued_pairs == 0 and honor_pairs_or_pons < 2: return False # doesn't matter valued or not, if we have just one honor pair # and have some single unvalued tiles, let's throw them away # first if honor_pairs_or_pons == 1 and unvalued_singles >= 2: return False # 3 non-isolated unsuitable tiles, 1-shanen and already 8th turn # let's not consider honitsu here if self.player.ai.shanten == 1 and self.player.round_step > 8: return False else: # we have a pon of unvalued honor doras, but it looks like # it's faster to build our hand without honitsu if self.player.ai.shanten == 1: return False # if we have a complete set in other suits, we can only throw it away if it's early in the game if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1: # too late to throw away chi after 8 step if self.player.round_step > 8: return False # already 1 shanten, no need to throw away complete set if self.player.ai.shanten == 1: return False # dora is not isolated and we have a complete set, let's not go for honitsu if self.dora_count_other_suits_not_isolated >= 1: return False self.chosen_suit = suit['function'] return True
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 should_activate_strategy(self, tiles_136): """ We can go for chinitsu strategy if we have prevalence of one suit """ result = super(ChinitsuStrategy, self).should_activate_strategy(tiles_136) if not result: return False # when making decisions about chinitsu, we should consider # the state of our own hand, tiles_34 = TilesConverter.to_34_array(self.player.tiles) suits = count_tiles_by_suits(tiles_34) suits = [x for x in suits if x['name'] != 'honor'] suits = sorted(suits, key=lambda x: x['count'], reverse=True) suit = suits[0] count_of_shuntsu_other_suits = 0 count_of_koutsu_other_suits = 0 count_of_shuntsu_other_suits += HonitsuStrategy._count_of_shuntsu(tiles_34, suits[1]['function']) count_of_shuntsu_other_suits += HonitsuStrategy._count_of_shuntsu(tiles_34, suits[2]['function']) count_of_koutsu_other_suits += HonitsuStrategy._count_of_koutsu(tiles_34, suits[1]['function']) count_of_koutsu_other_suits += HonitsuStrategy._count_of_koutsu(tiles_34, suits[2]['function']) # we need to have at least 9 tiles of one suit to fo for chinitsu if suit['count'] < 9: return False # here we only check doras in different suits, we will deal # with honors later self._initialize_chinitsu_dora_count(tiles_136, suit) # 3 non-isolated doras in other suits is too much # to even try if self.dora_count_not_suitable >= 3: return False if self.dora_count_not_suitable == 2: # 2 doras in other suits, no doras in our suit # let's not consider chinitsu if self.dora_count_suitable == 0: return False # we have 2 doras in other suits and we # are 1 shanten, let's not rush chinitsu if self.player.ai.shanten == 1: return False # too late to get rid of doras in other suits if self.player.round_step > 8: return False # we are almost tempai, chinitsu is slower if suit['count'] == 9 and self.player.ai.shanten == 1: return False # only 10 tiles by 8th turn is too slow, considering alternative if suit['count'] == 10 and self.player.ai.shanten == 1 and self.player.round_step > 8: return False # if we have a pon of honors, let's not go for chinitsu honor_pons = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] >= 3]) if honor_pons >= 1: return False # if we have a valued pair, let's not go for chinitsu valued_pairs = len([x for x in self.player.valued_honors if tiles_34[x] == 2]) if valued_pairs >= 1: return False # if we have a pair of honor doras, let's not go for chinitsu honor_doras_pairs = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] == 2 and plus_dora(x * 4, self.player.table.dora_indicators)]) if honor_doras_pairs >= 1: return False # if we have a honor pair, we will only throw them away if it's early in the game # and if we have lots of tiles in our suit honor_pairs = len([x for x in range(0, 34) if is_honor(x) and tiles_34[x] == 2]) if honor_pairs >= 2: return False if honor_pairs == 1: if suit['count'] < 11: return False if self.player.round_step > 8: return False # if we have a complete set in other suits, we can only throw it away if it's early in the game if count_of_shuntsu_other_suits + count_of_koutsu_other_suits >= 1: # too late to throw away chi after 8 step if self.player.round_step > 8: return False # already 1 shanten, no need to throw away complete set if self.player.round_step > 5 and self.player.ai.shanten == 1: return False # dora is not isolated and we have a complete set, let's not go for chinitsu if self.dora_count_not_suitable >= 1: return False self.chosen_suit = suit['function'] return True
def try_to_find_safe_tile_to_discard(self): discard_results, _ = self.player.ai.hand_builder.find_discard_options( self.player.tiles, self.player.closed_hand, self.player.melds ) self.hand_34 = TilesConverter.to_34_array(self.player.tiles) self.closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) threatening_players = self._get_threatening_players() # safe tiles that can be safe based on the table situation safe_tiles = self.impossible_wait.find_tiles_to_discard(threatening_players) # first try to check common safe tiles to discard for all players if len(threatening_players) > 1: against_honitsu = [] for player in threatening_players: if player.chosen_suit: against_honitsu += [self._mark_safe_tiles_against_honitsu(player)] common_safe_tiles = [x.all_safe_tiles for x in threatening_players] common_safe_tiles += against_honitsu # let's find a common tiles that will be safe against all threatening players common_safe_tiles = list(set.intersection(*map(set, common_safe_tiles))) common_safe_tiles = [DefenceTile(x, DefenceTile.SAFE) for x in common_safe_tiles] # there is no sense to calculate suji tiles for honitsu players not_honitsu_players = [x for x in threatening_players if x.chosen_suit is None] common_suji_tiles = self.suji.find_tiles_to_discard(not_honitsu_players) if common_safe_tiles: # it can be that safe tile will be mark as "almost safe", # but we already have "safe" tile in our hand validated_safe_tiles = common_safe_tiles for tile in safe_tiles: already_added_tile = [x for x in common_safe_tiles if x.value == tile.value] if not already_added_tile: validated_safe_tiles.append(tile) # first try to check 100% safe tiles for all players result = self._find_tile_to_discard(validated_safe_tiles, discard_results) if result: return result if common_suji_tiles: # if there is no 100% safe tiles try to check common suji tiles result = self._find_tile_to_discard(common_suji_tiles, discard_results) if result: return result # there are only one threatening player or we wasn't able to find common safe tiles # let's find safe tiles for most dangerous player first # and than for all other players if we failed find tile for dangerous player for player in threatening_players: player_safe_tiles = [DefenceTile(x, DefenceTile.SAFE) for x in player.player.all_safe_tiles] player_suji_tiles = self.suji.find_tiles_to_discard([player]) # it can be that safe tile will be mark as "almost safe", # but we already have "safe" tile in our hand validated_safe_tiles = player_safe_tiles for tile in safe_tiles: already_added_tile = [x for x in player_safe_tiles if x.value == tile.value] if not already_added_tile: validated_safe_tiles.append(tile) # better to not use suji for honitsu hands if not player.chosen_suit: validated_safe_tiles += player_suji_tiles result = self._find_tile_to_discard(validated_safe_tiles, discard_results) if result: return result # try to find safe tiles against honitsu if player.chosen_suit: against_honitsu = self._mark_safe_tiles_against_honitsu(player) against_honitsu = [DefenceTile(x, DefenceTile.SAFE) for x in against_honitsu] result = self._find_tile_to_discard(against_honitsu, discard_results) if result: return result # we wasn't able to find safe tile to discard return None
def calculate_second_level_ukeire(self, discard_option, tiles, melds): not_suitable_tiles = self.ai.current_strategy and self.ai.current_strategy.not_suitable_tiles or [] call_riichi = not self.player.is_open_hand # we are going to do manipulations that require player hand to be updated # so we save original tiles here and restore it at the end of the function player_tiles_original = self.player.tiles.copy() tile_in_hand = discard_option.find_tile_in_hand(self.player.closed_hand) self.player.tiles = tiles.copy() self.player.tiles.remove(tile_in_hand) sum_tiles = 0 sum_cost = 0 for wait_34 in discard_option.waiting: if self.player.is_open_hand and wait_34 in not_suitable_tiles: continue closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) live_tiles = 4 - self.player.total_tiles(wait_34, closed_hand_34) if live_tiles == 0: continue wait_136 = wait_34 * 4 self.player.tiles.append(wait_136) results, shanten = self.find_discard_options( self.player.tiles, self.player.closed_hand, melds ) results = [x for x in results if x.shanten == discard_option.shanten - 1] # let's take best ukeire here if results: result_has_atodzuke = False if self.player.is_open_hand: best_one = results[0] best_ukeire = 0 for result in results: has_atodzuke = False ukeire = 0 for wait_34 in result.waiting: if wait_34 in not_suitable_tiles: has_atodzuke = True else: ukeire += result.wait_to_ukeire[wait_34] # let's consider atodzuke waits to be worse than non-atodzuke ones if has_atodzuke: ukeire /= 2 if (ukeire > best_ukeire) or (ukeire >= best_ukeire and not has_atodzuke): best_ukeire = ukeire best_one = result result_has_atodzuke = has_atodzuke else: best_one = sorted(results, key=lambda x: -x.ukeire)[0] best_ukeire = best_one.ukeire sum_tiles += best_ukeire * live_tiles # if we are going to have a tempai (on our second level) - let's also count its cost if shanten == 0: next_tile_in_hand = best_one.find_tile_in_hand(self.player.closed_hand) self.player.tiles.remove(next_tile_in_hand) cost_x_ukeire, _ = self._estimate_cost_x_ukeire(best_one, call_riichi=call_riichi) # we reduce tile valuation for atodzuke if result_has_atodzuke: cost_x_ukeire /= 2 sum_cost += cost_x_ukeire self.player.tiles.append(next_tile_in_hand) self.player.tiles.remove(wait_136) discard_option.ukeire_second = sum_tiles if discard_option.shanten == 1: discard_option.second_level_cost = sum_cost # restore original state of player hand self.player.tiles = player_tiles_original
def start_game(self): log_link = '' # play in private or tournament lobby if settings.LOBBY != '0': if settings.IS_TOURNAMENT: logger.info('Go to the tournament lobby: {}'.format(settings.LOBBY)) self._send_message('<CS lobby="{}" />'.format(settings.LOBBY)) self._random_sleep(1, 2) self._send_message('<DATE />') else: logger.info('Go to the lobby: {}'.format(settings.LOBBY)) self._send_message('<CHAT text="{}" />'.format(quote('/lobby {}'.format(settings.LOBBY)))) self._random_sleep(1, 2) if self.reconnected_messages: # we already in the game self.looking_for_game = False self._send_message('<GOK />') self._random_sleep(1, 2) else: selected_game_type = self._build_game_type() game_type = '{},{}'.format(settings.LOBBY, selected_game_type) if not settings.IS_TOURNAMENT: self._send_message('<JOIN t="{}" />'.format(game_type)) logger.info('Looking for the game...') start_time = datetime.datetime.now() while self.looking_for_game: self._random_sleep(1, 2) messages = self._get_multiple_messages() for message in messages: if '<REJOIN' in message: # game wasn't found, continue to wait self._send_message('<JOIN t="{},r" />'.format(game_type)) if '<GO' in message: self._random_sleep(1, 2) self._send_message('<GOK />') self._send_message('<NEXTREADY />') # we had to have it there # because for tournaments we don't know # what exactly game type was set selected_game_type = self.decoder.parse_go_tag(message) process_rules = self._set_game_rules(selected_game_type) if not process_rules: logger.error('Hirosima (3 man) is not supported at the moment') self.end_game(success=False) return if '<TAIKYOKU' in message: self.looking_for_game = False game_id, seat = self.decoder.parse_log_link(message) log_link = 'http://tenhou.net/0/?log={}&tw={}'.format(game_id, seat) self.statistics.game_id = game_id if '<UN' in message: values = self.decoder.parse_names_and_ranks(message) self.table.set_players_names_and_ranks(values) self.statistics.username = values[0]['name'] if '<LN' in message: self._send_message(self._pxr_tag()) current_time = datetime.datetime.now() time_difference = current_time - start_time if time_difference.seconds > 60 * settings.WAITING_GAME_TIMEOUT_MINUTES: break # we wasn't able to find the game in specified time range # sometimes it happens and we need to end process # and try again later if self.looking_for_game: logger.error('Game is not started. Can\'t find the game') self.end_game() return logger.info('Game started') logger.info('Log: {}'.format(log_link)) logger.info('Players: {}'.format(self.table.players)) main_player = self.table.player meld_tile = None tile_to_discard = None while self.game_is_continue: self._random_sleep(1, 2) messages = self._get_multiple_messages() if self.reconnected_messages: messages = self.reconnected_messages + messages self.reconnected_messages = None if not messages: self._count_of_empty_messages += 1 else: # we had set to zero counter self._count_of_empty_messages = 0 for message in messages: if '<INIT' in message or '<REINIT' in message: values = self.decoder.parse_initial_values(message) self.table.init_round( values['round_wind_number'], values['count_of_honba_sticks'], values['count_of_riichi_sticks'], values['dora_indicator'], values['dealer'], values['scores'], ) logger.info('Round Log: {}&ts={}'.format(log_link, self.table.round_number)) tiles = self.decoder.parse_initial_hand(message) self.table.player.init_hand(tiles) logger.info(self.table) logger.info('Players: {}'.format(self.table.get_players_sorted_by_scores())) logger.info('Dealer: {}'.format(self.table.get_player(values['dealer']))) if '<REINIT' in message: players = self.decoder.parse_table_state_after_reconnection(message) for x in range(0, 4): player = players[x] for item in player['discards']: self.table.add_discarded_tile(x, item, False) for item in player['melds']: if x == 0: tiles = item.tiles main_player.tiles.extend(tiles) self.table.add_called_meld(x, item) # draw and discard if '<T' in message: win_suggestions = ['t="16"', 't="48"'] # we won by self draw (tsumo) if any(i in message for i in win_suggestions): self._random_sleep(1, 2) self._send_message('<N type="7" />') continue # Kyuushuu kyuuhai 「九種九牌」 # (9 kinds of honor or terminal tiles) if 't="64"' in message: self._random_sleep(1, 2) # TODO aim for kokushi self._send_message('<N type="9" />') continue drawn_tile = self.decoder.parse_tile(message) logger.info('Drawn tile: {}'.format(TilesConverter.to_one_line_string([drawn_tile]))) kan_type = self.player.should_call_kan(drawn_tile, False, main_player.in_riichi) if kan_type: self._random_sleep(1, 2) if kan_type == Meld.CHANKAN: meld_type = 5 logger.info('We upgraded pon to kan!') else: meld_type = 4 logger.info('We called a closed kan set!') self._send_message('<N type="{}" hai="{}" />'.format(meld_type, drawn_tile)) continue if not main_player.in_riichi: self.player.draw_tile(drawn_tile) discarded_tile = self.player.discard_tile() can_call_riichi = main_player.can_call_riichi() # let's call riichi if can_call_riichi: self._random_sleep(1, 2) self._send_message('<REACH hai="{}" />'.format(discarded_tile)) main_player.in_riichi = True else: # we had to add it to discards, to calculate remaining tiles correctly discarded_tile = drawn_tile self.table.add_discarded_tile(0, discarded_tile, True) # tenhou format: <D p="133" /> self._send_message('<D p="{}"/>'.format(discarded_tile)) logger.info('Discard: {}'.format(TilesConverter.to_one_line_string([discarded_tile]))) logger.info('Remaining tiles: {}'.format(self.table.count_of_remaining_tiles)) # new dora indicator after kan if '<DORA' in message: tile = self.decoder.parse_dora_indicator(message) self.table.add_dora_indicator(tile) logger.info('New dora indicator: {}'.format(TilesConverter.to_one_line_string([tile]))) if '<REACH' in message and 'step="1"' in message: who_called_riichi = self.decoder.parse_who_called_riichi(message) self.table.add_called_riichi(who_called_riichi) logger.info('Riichi called by {} player'.format(who_called_riichi)) # the end of round if '<AGARI' in message or '<RYUUKYOKU' in message: self._random_sleep(1, 2) self._send_message('<NEXTREADY />') # set was called if self.decoder.is_opened_set_message(message): meld = self.decoder.parse_meld(message) self.table.add_called_meld(meld.who, meld) logger.info('Meld: {} by {}'.format(meld, meld.who)) # tenhou confirmed that we called a meld # we had to do discard after this if meld.who == 0: if meld.type != Meld.KAN and meld.type != Meld.CHANKAN: discarded_tile = self.player.discard_tile(tile_to_discard) self.player.tiles.append(meld_tile) self._send_message('<D p="{}"/>'.format(discarded_tile)) win_suggestions = [ 't="8"', 't="9"', 't="10"', 't="11"', 't="12"', 't="13"', 't="15"' ] # we win by other player's discard if any(i in message for i in win_suggestions): # enemy called shouminkan and we can win there if self.decoder.is_opened_set_message(message): meld = self.decoder.parse_meld(message) tile = meld.called_tile enemy_seat = meld.who else: tile = self.decoder.parse_tile(message) enemy_seat = self.decoder.get_enemy_seat(message) self._random_sleep(1, 2) if main_player.should_call_win(tile, enemy_seat): self._send_message('<N type="6" />') else: self._send_message('<N />') if self.decoder.is_discarded_tile_message(message): tile = self.decoder.parse_tile(message) # <e21/> - is tsumogiri # <E21/> - discard from the hand if_tsumogiri = message[1].islower() player_seat = self.decoder.get_enemy_seat(message) # open hand suggestions if 't=' in message: # Possible t="" suggestions # 1 pon # 2 kan (it is a closed kan and can be send only to the self draw) # 3 pon + kan # 4 chi # 5 pon + chi # 7 pon + kan + chi # should we call a kan? if 't="3"' in message or 't="7"' in message: if self.player.should_call_kan(tile, True): self._random_sleep(1, 2) # 2 is open kan self._send_message('<N type="2" />') logger.info('We called an open kan set!') continue # player with "g" discard is always our kamicha is_kamicha_discard = False if message[1].lower() == 'g': is_kamicha_discard = True meld, tile_to_discard = self.player.try_to_call_meld(tile, is_kamicha_discard) if meld: self._random_sleep(1, 2) meld_tile = tile # 1 is pon meld_type = '1' if meld.type == Meld.CHI: # yeah it is 3, not 4 # because of tenhou protocol meld_type = '3' tiles = meld.tiles tiles.remove(meld_tile) # try to call a meld self._send_message('<N type="{}" hai0="{}" hai1="{}" />'.format( meld_type, tiles[0], tiles[1] )) # this meld will not improve our hand else: self._send_message('<N />') self.table.add_discarded_tile(player_seat, tile, if_tsumogiri) if 'owari' in message: values = self.decoder.parse_final_scores_and_uma(message) self.table.set_players_scores(values['scores'], values['uma']) if '<PROF' in message: self.game_is_continue = False # socket was closed by tenhou if self._count_of_empty_messages >= 5: logger.error('We are getting empty messages from socket. Probably socket connection was closed') self.end_game(False) return logger.info('Final results: {}'.format(self.table.get_players_sorted_by_scores())) # we need to finish the game, and only after this try to send statistics # if order will be different, tenhou will return 404 on log download endpoint self.end_game() # sometimes log is not available just after the game # let's wait one minute before the statistics update if settings.STAT_SERVER_URL: sleep(60) result = self.statistics.send_statistics() logger.info('Statistics sent: {}'.format(result))
def _should_call_riichi_one_sided(self): count_tiles = self.player.ai.hand_builder.count_tiles( self.player.ai.waiting, TilesConverter.to_34_array(self.player.closed_hand) ) waiting = self.player.ai.waiting[0] hand_value = self.player.ai.estimate_hand_value(waiting, call_riichi=False) tiles = self.player.closed_hand.copy() closed_melds = [x for x in self.player.melds if not x.opened] for meld in closed_melds: tiles.extend(meld.tiles[:3]) results, tiles_34 = self.player.ai.hand_builder.divide_hand(tiles, waiting) result = results[0] closed_tiles_34 = TilesConverter.to_34_array(self.player.closed_hand) have_suji, have_kabe = self.player.ai.hand_builder.check_suji_and_kabe(closed_tiles_34, waiting) # what if we have yaku if hand_value.yaku is not None and hand_value.cost is not None: min_cost = hand_value.cost['main'] # tanki honor is a good wait, let's damaten only if hand is already expensive if is_honor(waiting): if self.player.is_dealer and min_cost < 12000: return True if not self.player.is_dealer and min_cost < 8000: return True return False is_chiitoitsu = len([x for x in result if is_pair(x)]) == 7 simplified_waiting = simplify(waiting) for hand_set in result: if waiting not in hand_set: continue # tanki wait but not chiitoitsu if is_pair(hand_set) and not is_chiitoitsu: # let's not riichi tanki 4, 5, 6 if 3 <= simplified_waiting <= 5: return False # don't riichi tanki wait on 1, 2, 3, 7, 8, 9 if it's only 1 tile if count_tiles == 1: return False # don't riichi 2378 tanki if hand has good value if simplified_waiting != 0 and simplified_waiting != 8: if self.player.is_dealer and min_cost >= 7700: return False if not self.player.is_dealer and min_cost >= 5200: return False # only riichi if we have suji-trab or there is kabe if not have_suji and not have_kabe: return False return True # tanki wait with chiitoitsu if is_pair(hand_set) and is_chiitoitsu: # chiitoitsu on last suit tile is no the best if count_tiles == 1: return False # only riichi if we have suji-trab or there is kabe if not have_suji and not have_kabe: return False return True # 1-sided wait means kanchan or penchan if is_chi(hand_set): # let's not riichi kanchan on 4, 5, 6 if 3 <= simplified_waiting <= 5: return False # now checking waiting for 2, 3, 7, 8 # if we only have 1 tile to wait for, let's damaten if count_tiles == 1: return False # if we have 2 tiles to wait for and hand cost is good without riichi, # let's damaten if count_tiles == 2: if self.player.is_dealer and min_cost >= 7700: return False if not self.player.is_dealer and min_cost >= 5200: return False # only riichi if we have suji-trab or there is kabe if not have_suji and not have_kabe: return False return True # what if we don't have yaku # our tanki wait is good, let's riichi if is_honor(waiting): return True simplified_waiting = simplify(waiting) for hand_set in result: if waiting not in hand_set: continue if is_pair(hand_set): # let's not riichi tanki wait without suji-trap or kabe if not have_suji and not have_kabe: return False # let's not riichi tanki on last suit tile if it's early if count_tiles == 1 and self.player.round_step < 6: return False # let's not riichi tanki 4, 5, 6 if it's early if 3 <= simplified_waiting <= 5 and self.player.round_step < 6: return False # 1-sided wait means kanchan or penchan if is_chi(hand_set): # let's only riichi this bad wait if # it has all 4 tiles available or it # it's not too early if 4 <= simplified_waiting <= 6: return count_tiles == 4 or self.player.round_step >= 6 return True
def reproduce(self, dry_run=False): draw_tags = ['T', 'U', 'V', 'W'] discard_tags = ['D', 'E', 'F', 'G'] player_draw = draw_tags[self.player_position] player_draw_regex = re.compile('^<[{}]+\d*'.format(''.join(player_draw))) discard_regex = re.compile('^<[{}]+\d*'.format(''.join(discard_tags))) table = Table() for tag in self.round_content: if player_draw_regex.match(tag) and 'UN' not in tag: print('Player draw') tile = self.decoder.parse_tile(tag) table.player.draw_tile(tile) if dry_run: if self._is_draw(tag): print('<-', TilesConverter.to_one_line_string([self._parse_tile(tag)]), tag) elif self._is_discard(tag): print('->', TilesConverter.to_one_line_string([self._parse_tile(tag)]), tag) elif self._is_init_tag(tag): hands = { 0: [int(x) for x in self._get_attribute_content(tag, 'hai0').split(',')], 1: [int(x) for x in self._get_attribute_content(tag, 'hai1').split(',')], 2: [int(x) for x in self._get_attribute_content(tag, 'hai2').split(',')], 3: [int(x) for x in self._get_attribute_content(tag, 'hai3').split(',')], } print('Initial hand:', TilesConverter.to_one_line_string(hands[self.player_position])) else: print(tag) if not dry_run and tag == self.stop_tag: break if 'INIT' in tag: values = self.decoder.parse_initial_values(tag) shifted_scores = [] for x in range(0, 4): shifted_scores.append(values['scores'][self._normalize_position(x, self.player_position)]) table.init_round( values['round_wind_number'], values['count_of_honba_sticks'], values['count_of_riichi_sticks'], values['dora_indicator'], self._normalize_position(self.player_position, values['dealer']), shifted_scores, ) hands = [ [int(x) for x in self.decoder.get_attribute_content(tag, 'hai0').split(',')], [int(x) for x in self.decoder.get_attribute_content(tag, 'hai1').split(',')], [int(x) for x in self.decoder.get_attribute_content(tag, 'hai2').split(',')], [int(x) for x in self.decoder.get_attribute_content(tag, 'hai3').split(',')], ] table.player.init_hand(hands[self.player_position]) if discard_regex.match(tag) and 'DORA' not in tag: tile = self.decoder.parse_tile(tag) player_sign = tag.upper()[1] player_seat = self._normalize_position(self.player_position, discard_tags.index(player_sign)) if player_seat == 0: table.player.discard_tile(tile) else: table.add_discarded_tile(player_seat, tile, False) if '<N who=' in tag: meld = self.decoder.parse_meld(tag) player_seat = self._normalize_position(self.player_position, meld.who) table.add_called_meld(player_seat, meld) if player_seat == 0: # we had to delete called tile from hand # to have correct tiles count in the hand if meld.type != Meld.KAN and meld.type != Meld.CHANKAN: table.player.draw_tile(meld.called_tile) if '<REACH' in tag and 'step="1"' in tag: who_called_riichi = self._normalize_position(self.player_position, self.decoder.parse_who_called_riichi(tag)) table.add_called_riichi(who_called_riichi) if not dry_run: tile = self.decoder.parse_tile(self.stop_tag) print('Hand: {}'.format(table.player.format_hand_for_print(tile))) # to rebuild all caches table.player.draw_tile(tile) tile = table.player.discard_tile() # real run, you can stop debugger here table.player.draw_tile(tile) tile = table.player.discard_tile() print('Discard: {}'.format(TilesConverter.to_one_line_string([tile])))
def should_call_kan(self, tile, open_kan, from_riichi=False): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :param from_riichi: boolean :return: kan type """ # we can't call kan on the latest tile if self.table.count_of_remaining_tiles <= 1: return None # we don't need to add dora for other players if self.player.ai.in_defence: return None if open_kan: # we don't want to start open our hand from called kan if not self.player.is_open_hand: return None # there is no sense to call open kan when we are not in tempai if not self.player.in_tempai: return None # we have a bad wait, rinshan chance is low if len(self.waiting) < 2: 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) melds_34 = copy.copy(self.player.meld_34_tiles) tiles = copy.copy(self.player.tiles) closed_hand_tiles = copy.copy(self.player.closed_hand) new_shanten = 0 previous_shanten = 0 new_waits_count = 0 previous_waits_count = 0 # let's check can we upgrade opened pon to the kan pon_melds = [x for x in self.player.meld_34_tiles if is_pon(x)] has_shouminkan_candidate = False for meld in pon_melds: # tile is equal to our already opened pon if tile_34 in meld: has_shouminkan_candidate = True tiles.append(tile) closed_hand_tiles.append(tile) previous_shanten, previous_waits_count = self._calculate_shanten_for_kan( tiles, closed_hand_tiles, self.player.melds ) tiles_34 = TilesConverter.to_34_array(tiles) tiles_34[tile_34] -= 1 new_waiting, new_shanten = self.hand_builder.calculate_waits( tiles_34, self.player.meld_34_tiles ) new_waits_count = self.hand_builder.count_tiles(new_waiting, tiles_34) if not has_shouminkan_candidate: # we don't have enough tiles in the hand if closed_hand_34[tile_34] != 3: return None if open_kan or from_riichi: # this 4 tiles can only be used in kan, no other options previous_waiting, previous_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34) previous_waits_count = self.hand_builder.count_tiles(previous_waiting, closed_hand_34) else: tiles.append(tile) closed_hand_tiles.append(tile) previous_shanten, previous_waits_count = self._calculate_shanten_for_kan( tiles, closed_hand_tiles, self.player.melds ) # shanten calculator doesn't like working with kans, so we pretend it's a pon melds_34 += [[tile_34, tile_34, tile_34]] new_waiting, new_shanten = self.hand_builder.calculate_waits(tiles_34, melds_34) closed_hand_34[tile_34] = 4 new_waits_count = self.hand_builder.count_tiles(new_waiting, closed_hand_34) # it is possible that we don't have results here # when we are in agari state (but without yaku) if previous_shanten is None: return None # it is not possible to reduce number of shanten by calling a kan assert new_shanten >= previous_shanten # if shanten number is the same, we should only call kan if ukeire didn't become worse if new_shanten == previous_shanten: # we cannot improve ukeire by calling kan (not considering the tile we drew from the dead wall) assert new_waits_count <= previous_waits_count if new_waits_count == previous_waits_count: return has_shouminkan_candidate and Meld.CHANKAN or Meld.KAN return None