def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() self.shanten_calculator = Shanten() self.defence = DefenceHandler(player) self.riichi = Riichi(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.hand_builder = HandBuilder(player, self) self.erase_state()
def test_chinroutou(self): hand = HandCalculator() tiles = self._string_to_136_array(man="111999", sou="11999", pin="111") win_tile = self._string_to_136_tile(sou="9") melds = [ self._make_meld(Meld.PON, pin="111"), ] result = hand.estimate_hand_value( tiles, win_tile, melds=melds, scores_calculator_factory=Aotenjou, config=self._make_hand_config(is_tsumo=True, player_wind=EAST, round_wind=EAST), ) self.assertEqual(result.error, None) self.assertEqual(result.han, 15) self.assertEqual(result.fu, 50) self.assertEqual(len(result.yaku), 2) self.assertEqual(result.cost["main"] + result.cost["additional"], 26214400)
def test_shousuushii(self): hand = HandCalculator() tiles = self._string_to_136_array(sou="123", honors="11122233444") win_tile = self._string_to_136_tile(honors="2") melds = [ self._make_meld(Meld.PON, honors="444"), ] result = hand.estimate_hand_value( tiles, win_tile, melds=melds, scores_calculator_factory=Aotenjou, config=self._make_hand_config(is_tsumo=True, player_wind=EAST, round_wind=EAST), ) self.assertEqual(result.error, None) self.assertEqual(result.han, 18) self.assertEqual(result.fu, 50) self.assertEqual(len(result.yaku), 5) self.assertEqual(result.cost["main"] + result.cost["additional"], 209715200)
def test_sextuple_yakuman(self): hand = HandCalculator() # 1112223334445z 5z tenhou & tsuisou & daisushi & suuankou tanki tiles = self._string_to_136_array(honors="11122233344455") win_tile = self._string_to_136_tile(honors="5") config = self._make_hand_config(is_tsumo=True, is_tenhou=True, disable_double_yakuman=False) result = hand.estimate_hand_value(tiles, win_tile, config=config) self.assertEqual(result.error, None) self.assertEqual(result.han, 78) self.assertEqual(result.cost["main"], 96000) self.assertEqual(result.cost["additional"], 48000) # 5z -11-z -22-z -33-z -44-z 5z suukantsu & tsuisou & daisushi & suuankou tanki tiles = self._string_to_136_array(honors="111122223333444455") win_tile = self._string_to_136_tile(honors="5") config = self._make_hand_config(disable_double_yakuman=False) melds = [ self._make_meld(Meld.KAN, is_open=False, honors="1111"), self._make_meld(Meld.KAN, is_open=False, honors="2222"), self._make_meld(Meld.KAN, is_open=False, honors="3333"), self._make_meld(Meld.KAN, is_open=False, honors="4444"), ] result = hand.estimate_hand_value(tiles, win_tile, melds=melds, config=config) self.assertEqual(result.error, None) self.assertEqual(result.han, 78) self.assertEqual(result.cost["main"], 192000)
def test_is_suuankou(self): hand = HandCalculator() tiles = self._string_to_34_array(sou="111444", man="333", pin="44555") win_tile = self._string_to_136_tile(sou="4") self.assertTrue( self.config.suuankou.is_condition_met(self._hand(tiles), win_tile, True)) self.assertFalse( self.config.suuankou.is_condition_met(self._hand(tiles), win_tile, False)) tiles = self._string_to_136_array(sou="111444", man="333", pin="44555") win_tile = self._string_to_136_tile(pin="5") result = hand.estimate_hand_value( tiles, win_tile, config=self._make_hand_config(is_tsumo=True)) self.assertEqual(result.error, None) self.assertEqual(result.han, 13) self.assertEqual(result.fu, 50) self.assertEqual(len(result.yaku), 1) result = hand.estimate_hand_value( tiles, win_tile, config=self._make_hand_config(is_tsumo=False)) self.assertNotEqual(result.han, 13) tiles = self._string_to_136_array(sou="111444", man="333", pin="44455") win_tile = self._string_to_136_tile(pin="5") result = hand.estimate_hand_value( tiles, win_tile, config=self._make_hand_config(is_tsumo=True)) self.assertEqual(result.error, None) self.assertEqual(result.han, 26) self.assertEqual(result.fu, 50) self.assertEqual(len(result.yaku), 1) tiles = self._string_to_136_array(man="33344455577799") win_tile = self._string_to_136_tile(man="9") result = hand.estimate_hand_value( tiles, win_tile, config=self._make_hand_config(is_tsumo=False)) self.assertEqual(result.error, None) self.assertEqual(result.han, 26) self.assertEqual(result.fu, 50) self.assertEqual(len(result.yaku), 1)
def test_game(ai): pai = TEST_INIT_PAI.copy() random.shuffle(pai) calculator = HandCalculator() config = HandConfig(is_tsumo=True) config.yaku.yakuhai_place = config.yaku.east config.yaku.yakuhai_round = config.yaku.east # 1. 내 초기 패 13개 + 쯔모 1개 hand = pai[:14].copy() tsumo = pai[13] # 1 - a. nn_ai를 돌리는 경우에 env에 정보를 입력한다. err = 0 isGameOver = False # 2. tsumo 하자 for x in range(1, TEST_STEP + 1): # DEBUG: 정상 작동하는지 중간 패 과정 출력 logging.debug("%02d: %s", x, TilesConverter.to_one_line_string(hand)) # 점수 확인 result = calculator.estimate_hand_value(hand, tsumo) if not result.error: # 화료가 되었다는 뜻이다. 친의 쯔모인 3배 점수를 반환하자 isGameOver = True return result.cost['main'] * 3 if x == TEST_STEP: break ''' # 여기부터 # 버린다 discard = ai.next_move(hand, tsumo, TEST_STEP - x - 1) # 일반적인 경우 이거 쓰기 hand.remove(discard) # 쯔모한다 tsumo = pai[14 + x] hand.append(tsumo) # 여기까지는 nn_ai 아닐때 쓰는 코드 ''' #여기부터 discard_action = ai.next_move(hand, tsumo, TEST_STEP - x - 1, env, X, output_layer, nbActions, isGameOver, epsilon) #nn_ai이면 이 코드로 변경 discard = hand[discard_action] currentState = hand.copy() hand.remove(discard) tsumo = pai[14 + x] hand.append(tsumo) nextState = hand.copy() result = calculator.estimate_hand_value(hand, tsumo) if not result.error: isGameOver = True reward = result.cost['main'] * 3 else: hand_34 = TilesConverter.to_34_array(hand) shanten = Shanten() result = shanten.calculate_shanten(hand_34) logging.debug('@: %d', result) if result == 0: reward = TEST_TEN_SCORE reward = TEST_NOTEN_SCORE memory.Reward_Handling(currentState, discard_action, reward, nextState, isGameOver, env, err, epsilon, epsilonMinimumValue, output_layer, batchSize, nbActions, nbStates, X, Y, optimizer, cost) #여기까지는 nn_ai쓸때만 쓰는 코드 # 마지막으로 텐파이인지 확인한다 # 샹텐수가 0이면 텐파이이다 hand_34 = TilesConverter.to_34_array(hand) shanten = Shanten() result = shanten.calculate_shanten(hand_34) logging.debug('@: %d', result) if result == 0: return TEST_TEN_SCORE return TEST_NOTEN_SCORE
class Phoenix: def __init__(self, player): self.player = player self.table = player.table self.chi = Chi(player) self.pon = Pon(player) self.kan = Kan(player) self.riichi = Riichi(player) self.discard = Discard(player) self.grp = GlobalRewardPredictor() self.hand_builder = HandBuilder(player, self) self.shanten_calculator = Shanten() self.hand_cache_shanten = {} self.placement = player.config.PLACEMENT_HANDLER_CLASS(player) self.finished_hand = HandCalculator() self.hand_divider = HandDivider() self.erase_state() def erase_state(self): self.hand_cache_shanten = {} self.hand_cache_estimation = {} self.finished_hand = HandCalculator() self.grp_features = [] def collect_experience(self): #collect round info init_scores = np.array(self.table.init_scores) / 1e5 gains = np.array(self.table.gains) / 1e5 dans = np.array( [RANKS.index(p.rank) for p in self.player.table.players]) dealer = int(self.player.dealer_seat) repeat_dealer = self.player.table.count_of_honba_sticks riichi_bets = self.player.table.count_of_riichi_sticks features = np.concatenate( (init_scores, gains, dans, np.array([dealer, repeat_dealer, riichi_bets])), axis=0) self.grp_features.append(features) #prepare input grp_input = [np.zeros(pred_emb_dim)] * max( round_num - len(self.grp_features), 0) + self.grp_features[:] reward = self.grp.get_global_reward( np.expand_dims(np.asarray(grp_input), axis=0))[0][self.player.seat] for model in [self.chi, self.pon, self.kan, self.riichi, self.discard]: model.collector.complete_episode(reward) def write_buffer(self): for model in [self.chi, self.pon, self.kan, self.riichi, self.discard]: model.collector.to_buffer() def init_hand(self): self.player.logger.debug( log.INIT_HAND, context=[ f"Round wind: {DISPLAY_WINDS[self.table.round_wind_tile]}", f"Player wind: {DISPLAY_WINDS[self.player.player_wind]}", f"Hand: {self.player.format_hand_for_print()}", ], ) self.shanten, _ = self.hand_builder.calculate_shanten_and_decide_hand_structure( TilesConverter.to_34_array(self.player.tiles)) def draw_tile(self): pass def discard_tile(self, discard_tile): ''' return discarded_tile and with_riichi ''' if discard_tile is not None: #discard after meld return discard_tile, False if self.player.is_open_hand: #can not riichi return self.discard.discard_tile(), False shanten = self.calculate_shanten_or_get_from_cache( TilesConverter.to_34_array(self.player.closed_hand)) if shanten != 0: #can not riichi return self.discard.discard_tile(), False with_riichi, p = self.riichi.should_call_riichi() if with_riichi: # fix here: might need review riichi_options = [ tile for tile in self.player.closed_hand if self.calculate_shanten_or_get_from_cache( TilesConverter.to_34_array( [t for t in self.player.closed_hand if t != tile])) == 0 ] tile_to_discard = self.discard.discard_tile( with_riichi_options=riichi_options) else: tile_to_discard = self.discard.discard_tile() return tile_to_discard, with_riichi def try_to_call_meld(self, tile_136, is_kamicha_discard, meld_type): # 1 pon # 2 kan (it is a closed kan and can be send only to the self draw) # 4 chi # there is two return value, meldPrint() and discardOption(), # while the second would not be used by client.py meld_chi, meld_pon = None, None should_chi, should_pon = False, False # print(tile_136) # print(self.player.closed_hand) melds_chi, melds_pon = self.get_possible_meld(tile_136, is_kamicha_discard) if melds_chi and meld_type & 4: should_chi, chi_score, tiles_chi = self.chi.should_call_chi( tile_136, melds_chi) # fix here: tiles_chi is now the first possible meld ---fixed! # tiles_chi = melds_chi[0] meld_chi = Meld(meld_type="chi", tiles=tiles_chi) if meld_chi else None if melds_pon and meld_type & 1: should_pon, pon_score = self.pon.should_call_pon( tile_136, is_kamicha_discard) tiles_pon = melds_pon[0] meld_pon = Meld(meld_type="pon", tiles=tiles_pon) if meld_pon else None if not should_chi and not should_pon: return None, None if should_chi and should_pon: meld = meld_chi if chi_score > pon_score else meld_pon elif should_chi: meld = meld_chi else: meld = meld_pon all_tiles_copy, meld_tiles_copy = self.player.tiles[:], self.player.meld_tiles[:] all_tiles_copy.append(tile_136) meld_tiles_copy.append(meld) closed_hand_copy = [ item for item in all_tiles_copy if item not in meld_tiles_copy ] discard_option = self.discard.discard_tile( all_hands_136=all_tiles_copy, closed_hands_136=closed_hand_copy) return meld, discard_option def should_call_kyuushu_kyuuhai(self): #try kokushi strategy if with 10 types tiles_34 = TilesConverter.to_34_array(self.player.tiles) types = sum([1 for t in tiles_34 if t > 0]) if types >= 10: return False else: return True def should_call_win(self, tile, is_tsumo, enemy_seat=None, is_chankan=False): # don't skip win in riichi if self.player.in_riichi: return True # currently we don't support win skipping for tsumo if is_tsumo: return True # fast path - check it first to not calculate hand cost cost_needed = self.placement.get_minimal_cost_needed() if cost_needed == 0: return True # 1 and not 0 because we call check for win this before updating remaining tiles is_hotei = self.player.table.count_of_remaining_tiles == 1 hand_response = self.calculate_exact_hand_value_or_get_from_cache( tile, tiles=self.player.tiles, call_riichi=self.player.in_riichi, is_tsumo=is_tsumo, is_chankan=is_chankan, is_haitei=is_hotei, ) assert hand_response is not None assert not hand_response.error, hand_response.error cost = hand_response.cost return self.placement.should_call_win(cost, is_tsumo, enemy_seat) def calculate_shanten_or_get_from_cache(self, closed_hand_34: List[int], use_chiitoitsu=True): """ Sometimes we are calculating shanten for the same hand multiple times to save some resources let's cache previous calculations """ key = build_shanten_cache_key(closed_hand_34, use_chiitoitsu) if key in self.hand_cache_shanten: return self.hand_cache_shanten[key] # if use_chiitoitsu and not self.player.is_open_hand: # result = self.shanten_calculator.calculate_shanten_for_chiitoitsu_hand(closed_hand_34) # else: # result = self.shanten_calculator.calculate_shanten_for_regular_hand(closed_hand_34) # fix here: a little bit strange in use_chiitoitsu shanten_results = [] if use_chiitoitsu and not self.player.is_open_hand: shanten_results.append( self.shanten_calculator.calculate_shanten_for_chiitoitsu_hand( closed_hand_34)) shanten_results.append( self.shanten_calculator.calculate_shanten_for_regular_hand( closed_hand_34)) result = min(shanten_results) self.hand_cache_shanten[key] = result return result def calculate_exact_hand_value_or_get_from_cache( self, win_tile_136, tiles=None, call_riichi=False, is_tsumo=False, is_chankan=False, is_haitei=False, is_ippatsu=False, ): if not tiles: tiles = self.player.tiles[:] else: tiles = tiles[:] if win_tile_136 not in tiles: tiles += [win_tile_136] additional_han = 0 if is_chankan: additional_han += 1 if is_haitei: additional_han += 1 if is_ippatsu: additional_han += 1 config = HandConfig( is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind_tile, is_tsumo=is_tsumo, options=OptionalRules( has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao, has_double_yakuman=False, ), is_chankan=is_chankan, is_ippatsu=is_ippatsu, is_haitei=is_tsumo and is_haitei or False, is_houtei=(not is_tsumo) and is_haitei or False, tsumi_number=self.player.table.count_of_honba_sticks, kyoutaku_number=self.player.table.count_of_riichi_sticks, ) return self._estimate_hand_value_or_get_from_cache( win_tile_136, tiles, call_riichi, is_tsumo, additional_han, config) def _estimate_hand_value_or_get_from_cache(self, win_tile_136, tiles, call_riichi, is_tsumo, additional_han, config, is_rinshan=False, is_chankan=False): cache_key = build_estimate_hand_value_cache_key( tiles, call_riichi, is_tsumo, self.player.melds, self.player.table.dora_indicators, self.player.table.count_of_riichi_sticks, self.player.table.count_of_honba_sticks, additional_han, is_rinshan, is_chankan, ) if self.hand_cache_estimation.get(cache_key): return self.hand_cache_estimation.get(cache_key) result = self.finished_hand.estimate_hand_value( tiles, win_tile_136, self.player.melds, self.player.table.dora_indicators, config, use_hand_divider_cache=True, ) self.hand_cache_estimation[cache_key] = result return result @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:] def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ pass def get_possible_meld(self, tile, is_kamicha_discard): closed_hand = self.player.closed_hand[:] # we can't open hand anymore if len(closed_hand) == 1: return None, None discarded_tile = tile // 4 closed_hand_34 = TilesConverter.to_34_array(closed_hand + [tile]) combinations = [] first_index = 0 second_index = 0 if is_man(discarded_tile): first_index = 0 second_index = 8 elif is_pin(discarded_tile): first_index = 9 second_index = 17 elif is_sou(discarded_tile): first_index = 18 second_index = 26 if second_index == 0: # honor tiles if closed_hand_34[discarded_tile] == 3: combinations = [[[discarded_tile] * 3]] else: # to avoid not necessary calculations # we can check only tiles around +-2 discarded tile first_limit = discarded_tile - 2 if first_limit < first_index: first_limit = first_index second_limit = discarded_tile + 2 if second_limit > second_index: second_limit = second_index combinations = self.hand_divider.find_valid_combinations( closed_hand_34, first_limit, second_limit, True) if combinations: combinations = combinations[0] # possible_melds = [] melds_chi, melds_pon = [], [] for best_meld_34 in combinations: # we can call pon from everyone if is_pon(best_meld_34) and discarded_tile in best_meld_34: if best_meld_34 not in melds_pon: melds_pon.append(best_meld_34) # we can call chi only from left player if is_chi( best_meld_34 ) and is_kamicha_discard and discarded_tile in best_meld_34: if best_meld_34 not in melds_chi: melds_chi.append(best_meld_34) return melds_chi, melds_pon def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ pass
def test_kokushi_musou_multiple_yakuman(self): hand_calculator = HandCalculator() # kokushi test tiles = TilesConverter.string_to_136_array(sou="19", pin="19", man="19", honors="12345677") win_tile = TilesConverter.string_to_136_array(honors="1")[0] hand_config = HandConfig(is_tsumo=True, is_tenhou=False, is_chiihou=False) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 1) self.assertTrue(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertFalse( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 13) hand_config = HandConfig(is_tsumo=True, is_tenhou=True, is_chiihou=False) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 2) self.assertTrue(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertFalse( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertTrue(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 26) hand_config = HandConfig(is_tsumo=True, is_tenhou=False, is_chiihou=True) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 2) self.assertTrue(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertFalse( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertTrue(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 26) # double kokushi test tiles = TilesConverter.string_to_136_array(sou="19", pin="19", man="19", honors="12345677") win_tile = TilesConverter.string_to_136_array(honors="7")[0] hand_config = HandConfig(is_tsumo=True, is_tenhou=False, is_chiihou=False) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 1) self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertTrue( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 26) hand_config = HandConfig(is_tsumo=True, is_tenhou=True, is_chiihou=False) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 2) self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertTrue( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertTrue(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 39) hand_config = HandConfig(is_tsumo=True, is_tenhou=False, is_chiihou=True) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 2) self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertTrue( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertTrue(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 39) hand_config = HandConfig(is_tsumo=False, is_renhou=True, options=OptionalRules(renhou_as_yakuman=True)) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 2) self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertTrue( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertTrue( hand_config.yaku.renhou_yakuman in hand_calculation.yaku) self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 39) hand_config = HandConfig( is_tsumo=False, is_renhou=True, is_riichi=True, is_open_riichi=True, options=OptionalRules(renhou_as_yakuman=True, has_sashikomi_yakuman=True), ) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 3) self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertTrue( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertTrue( hand_config.yaku.renhou_yakuman in hand_calculation.yaku) self.assertTrue(hand_config.yaku.sashikomi in hand_calculation.yaku) self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 52)
def test_kokushi_musou_multiple_yakuman(self): hand_calculator = HandCalculator() # kokushi test tiles = TilesConverter.string_to_136_array(sou='19', pin='19', man='19', honors='12345677') win_tile = TilesConverter.string_to_136_array(honors='1')[0] hand_config = HandConfig(is_tsumo=True, is_tenhou=False, is_chiihou=False) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 1) self.assertTrue(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertFalse( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 13) hand_config = HandConfig(is_tsumo=True, is_tenhou=True, is_chiihou=False) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 2) self.assertTrue(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertFalse( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertTrue(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 26) hand_config = HandConfig(is_tsumo=True, is_tenhou=False, is_chiihou=True) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 2) self.assertTrue(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertFalse( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertTrue(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 26) # double kokushi test tiles = TilesConverter.string_to_136_array(sou='19', pin='19', man='19', honors='12345677') win_tile = TilesConverter.string_to_136_array(honors='7')[0] hand_config = HandConfig(is_tsumo=True, is_tenhou=False, is_chiihou=False) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 1) self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertTrue( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 26) hand_config = HandConfig(is_tsumo=True, is_tenhou=True, is_chiihou=False) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 2) self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertTrue( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertTrue(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertFalse(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 39) hand_config = HandConfig(is_tsumo=True, is_tenhou=False, is_chiihou=True) hand_calculation = hand_calculator.estimate_hand_value( tiles, win_tile, config=hand_config) self.assertIsNone(hand_calculation.error) self.assertEqual(len(hand_calculation.yaku), 2) self.assertFalse(hand_config.yaku.kokushi in hand_calculation.yaku) self.assertTrue( hand_config.yaku.daburu_kokushi in hand_calculation.yaku) self.assertFalse(hand_config.yaku.tenhou in hand_calculation.yaku) self.assertTrue(hand_config.yaku.chiihou in hand_calculation.yaku) self.assertEqual(hand_calculation.han, 39)
from mahjong.hand_calculating.hand import HandCalculator from mahjong.meld import Meld from mahjong.hand_calculating.hand_config import HandConfig, OptionalRules from mahjong.shanten import Shanten from mahjong.tile import TilesConverter from mahjong.hand_calculating.divider import HandDivider from mahjong.constants import WINDS import json calculator = HandCalculator() def yaku_json_create(x, opened): if opened: return {"name": x.name, "japanese": x.japanese, "han": x.han_open} else: return {"name": x.name, "japanese": x.japanese, "han": x.han_closed} def yaku_is_yakuman(yaku): for x in yaku: if x.is_yakuman: return "役満" return "役満なし" # useful helper def print_hand_result(hand_result, opened=False): #print(hand_result.han, hand_result.fu) #print(hand_result.cost['main']) #print(hand_result.yaku)
class HandSolver(object): tiles = [] Shanten_calculater = None shanten = 8 Tiles = None improve_tiles_count = 0 Hand_Calculator = None cnt = 0 Hand_Config = None dora_indicators = [] revealed_tiles = None cnt = 0 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 choose_tile_to_discard(self): if self.shanten > 1: return self.pure_speed_strategy() max_value = 0 option_list = self.get_option_list(self.tiles) temp_tiles = list(self.tiles) option_value = [0] * 34 for option in option_list: while option not in temp_tiles: option += 1 temp_tiles.remove(option) option_value[option // 4] = self.estimate_value( temp_tiles, self.shanten, 1, self.shanten) temp_tiles.append(option) max_value = max(option_value) result = option_value.index(max_value) * 4 while result not in self.tiles: result += 1 return result def sort_tiles(self): if self.shanten == 0: return 'IS_TEMPAI' def calculate_Improve_tiles_count(self, tiles): cnt = 0 for tile in range(0, 135, 4): now_shanten = self.Shanten_calculater.calculate_shanten( self.Tiles.to_34_array(tiles)) tiles.append(tile) if self.Shanten_calculater.calculate_shanten( self.Tiles.to_34_array(tiles)) < now_shanten: cnt += 4 - self.revealed_tiles[tile // 4] tiles.remove(tile) return cnt def get_option_list(self, tiles): #the input are 14-tiles temp_tiles = list(tiles) option_list = [] current_shanten = self.Shanten_calculater.calculate_shanten( self.Tiles.to_34_array(temp_tiles)) for tile in tiles: if tile // 4 * 4 in option_list: continue temp_tiles.remove(tile) if self.Shanten_calculater.calculate_shanten( self.Tiles.to_34_array(temp_tiles)) <= current_shanten: option_list.append(tile // 4 * 4) temp_tiles.append(tile) return option_list def pure_speed_strategy(self): max_improve_tiles_count = 0 answer = 0 option_list = self.get_option_list(self.tiles) temp_tiles = list(self.tiles) for tile in option_list: while tile not in temp_tiles: tile += 1 temp_tiles.remove(tile) temp_improve_tiles_count = self.calculate_Improve_tiles_count( temp_tiles) if max_improve_tiles_count <= temp_improve_tiles_count: max_improve_tiles_count = temp_improve_tiles_count answer = tile temp_tiles.append(tile) return answer def solve_tempai(self): win_tiles = [] temp_tiles = list(self.tiles) have_han = 13 for tile in self.tiles: temp_tiles.remove(tile) if self.Shanten_calculater.calculate_shanten( self.Tiles.to_34_array(temp_tiles)) <= self.shanten: option_list.append(tile) temp_tiles.append(tile) for options in option_list: temp_tiles.remove(option) for tile in range(0, 135): temp_tiles.append(tile) if self.Shanten_calculater.calculate_shanten( self.Tiles.to_34_array(temp_tiles)) == -1: have_han = min( self.Hand_Calculator.estimate_hand_value(temp_tiles), have_han) temp_tiles.remove(tile) temp_tiles.append(option) if have_han == 0: pass def get_simple_win_rate(self, win_tile, is_jinpai, remain_number): rate = 0 if win_tile == 'Middle_Tile': if is_jinpai == 0: rate = 0.37 elif is_jinpai == 1: rate = 0.40 elif is_jinpai == 2: rate = 0.5 rate -= (4 - remain_number) * 0.07 elif win_tile == 'Three_Like_Tiles': if is_jinpai == 0: rate = 0.43 else: rate = 0.5 rate -= (4 - remain_number) * 0.07 elif win_tile == 'Two_Like_Tiles': if is_jinpai == 0: rate = 0.46 else: rate = 0.55 rate -= (4 - remain_number) * 0.07 elif win_tile == 'One_Like_Tiles': if is_jinpai == 0: rate = 0.5 else: rate = 0.6 rate -= (4 - remain_number) * 0.03 elif win_tile == 'Honor_Tiles': if remain_number == 2: rate = 0.6 else: rate = 0.55 return rate def get_win_rate(self): pass def get_sort_of_tile(self, tile): temp_tiles = [] temp_tiles.append(tile) temp_tile_string = self.Tiles.to_one_line_string(temp_tiles) middle_tiles = ['4m', '5m', '6m', '4s', '5s', '6s', '4p', '5p', '6p'] three_like_tiles = ['3m', '7m', '3p', '7p', '3s', '7s'] two_like_tiles = ['2m', '8m', '2p', '8p', '2s', '8s'] one_like_tiles = ['1m', '9m', '1s', '9s', '1p', '9p'] if temp_tile_string in middle_tiles: return 'Middle_Tile' elif temp_tile_string in three_like_tiles: return 'Three_Like_Tiles' elif temp_tile_string in two_like_tiles: return 'Two_Like_Tiles' elif temp_tile_string in one_like_tiles: return 'One_Like_Tiles' else: return 'Honor_Tiles' def estimate_value(self, tiles, current_shanten, depth, initial_shanten): #the input are 13 tiles value = 0 temp_tiles = list(tiles) if current_shanten == 0: return self.estimate_tempai_value(tiles) for tile in range(0, 135, 4): temp_tiles.append(tile) #search changes and turn to new tiles feature_shanten = self.Shanten_calculater.calculate_shanten( self.Tiles.to_34_array(temp_tiles)) if feature_shanten < current_shanten: option_list = self.get_option_list(temp_tiles) flag = 0 temp_tiles.remove(tile) current_improve_tiles_count = self.calculate_Improve_tiles_count( temp_tiles) max_improve_tiles_count = current_improve_tiles_count temp_tiles.append(tile) if feature_shanten == current_shanten: #在分析改良 for option in option_list: while option not in temp_tiles: option += 1 temp_tiles.remove(option) max_improve_tiles_count = max( max_improve_tiles_count, self.calculate_Improve_tiles_count(temp_tiles)) temp_tiles.append(option) if max_improve_tiles_count == current_improve_tiles_count: temp_tiles.remove(tile) continue max_value = 0 for option in option_list: while option not in temp_tiles: option += 1 temp_tiles.remove(option) if feature_shanten == current_shanten: max_value = max( self.estimate_value(temp_tiles, feature_shanten, depth + 1, initial_shanten), max_value) else: max_value = max( self.estimate_value(temp_tiles, feature_shanten, depth, initial_shanten), max_value) temp_tiles.append(option) value += max_value * (4 - self.revealed_tiles[tile // 4]) temp_tiles.remove(tile) return value / 100 def estimate_tempai_value(self, tiles): #the input are 13 tiles feature_value = 0 for tile in range(0, 135, 4): tiles.append(tile) if self.Shanten_calculater.calculate_shanten( self.Tiles.to_34_array(tiles)) == -1: result = self.Hand_Calculator.estimate_hand_value( tiles, tile, dora_indicators=self.dora_indicators, config=self.Hand_Config) feature_value += result.cost['main'] * ( 4 - self.revealed_tiles[tile // 4]) tiles.remove(tile) return feature_value
def test_aotenjou_hands(self): hand = HandCalculator() tiles = self._string_to_136_array(sou="119", man="19", pin="19", honors="1234567") win_tile = self._string_to_136_tile(sou="1") result = hand.estimate_hand_value( tiles, win_tile, scores_calculator_factory=Aotenjou, config=self._make_hand_config(player_wind=EAST, round_wind=EAST, disable_double_yakuman=True), ) self.assertEqual(result.error, None) self.assertEqual(result.han, 13) self.assertEqual(result.fu, 40) self.assertEqual(len(result.yaku), 1) self.assertEqual(result.cost["main"], 7864400) tiles = self._string_to_136_array(man="234", honors="11122233344") win_tile = self._string_to_136_tile(man="2") melds = [ self._make_meld(Meld.PON, honors="111"), self._make_meld(Meld.PON, honors="333"), ] result = hand.estimate_hand_value( tiles, win_tile, melds=melds, scores_calculator_factory=Aotenjou, config=self._make_hand_config(is_tsumo=True, player_wind=EAST, round_wind=EAST), ) self.assertEqual(result.error, None) self.assertEqual(result.han, 17) self.assertEqual(result.fu, 40) self.assertEqual(len(result.yaku), 4) self.assertEqual(result.cost["main"] + result.cost["additional"], 83886200) tiles = self._string_to_136_array(honors="11122233444777") win_tile = self._string_to_136_tile(honors="2") melds = [ self._make_meld(Meld.PON, honors="444"), ] result = hand.estimate_hand_value( tiles, win_tile, melds=melds, scores_calculator_factory=Aotenjou, config=self._make_hand_config(is_tsumo=True, player_wind=EAST, round_wind=EAST), ) self.assertEqual(result.error, None) self.assertEqual(result.han, 31) self.assertEqual(result.fu, 50) self.assertEqual(len(result.yaku), 6) self.assertEqual(result.cost["main"] + result.cost["additional"], 1717986918400) # monster hand for fun tiles = self._string_to_136_array(honors="111133555566667777") win_tile = self._string_to_136_tile(honors="3") melds = [ self._make_meld(Meld.KAN, honors="1111", is_open=False), self._make_meld(Meld.KAN, honors="5555", is_open=False), self._make_meld(Meld.KAN, honors="6666", is_open=False), self._make_meld(Meld.KAN, honors="7777", is_open=False), ] result = hand.estimate_hand_value( tiles, win_tile, melds=melds, dora_indicators=self._string_to_136_array(honors="22224444"), scores_calculator_factory=Aotenjou, config=self._make_hand_config(is_riichi=True, is_tsumo=True, is_ippatsu=True, is_haitei=True, player_wind=EAST, round_wind=EAST), ) self.assertEqual(result.error, None) self.assertEqual(result.han, 95) self.assertEqual(result.fu, 160) self.assertEqual(len(result.yaku), 11) self.assertEqual(result.cost["main"] + result.cost["additional"], 101412048018258352119736256430200)
from mahjong.hand_calculating.hand import HandCalculator from mahjong.tile import TilesConverter calculator = HandCalculator() # useful helper def print_hand_result(hand_results): for result in hand_results: print(result) print('') test_tiles = TilesConverter.string_to_72_array(tongzi='111123', tiaozi='55667788') results = calculator.estimate_hand_value_zigong(test_tiles, 5) print_hand_result(results)
class GameManager: """ Allow to play bots between each other To have a metrics how new version plays against old versions """ replay_name = "" tiles = None dead_wall = None clients = None dora_indicators = None players_with_open_hands = None discards = None replay = None dealer = None current_client_seat = None round_number = 0 honba_sticks = 0 riichi_sticks = 0 _unique_dealers = 0 _need_to_check_same_winds = None def __init__(self, clients, replays_directory, replay_name): self.tiles = [] self.dead_wall = [] self.dora_indicators = [] self.discards = [] self.clients = clients self.agari = Agari() self.finished_hand = HandCalculator() self.replays_directory = replays_directory self.replay_name = replay_name @staticmethod def generate_replay_name(): return f"{datetime.datetime.now().strftime('%Y-%m-%d_%H_%M_%S')}_{randint(0, 99999):03}.txt" def init_game(self): """ Beginning of the game. Clients random placement and dealer selection. """ logger.info("Replay name: {}".format(self.replay_name)) self.replay = TenhouReplay(self.replay_name, self.clients, self.replays_directory) seed(shuffle_seed()) self.clients = self._randomly_shuffle_array(self.clients) for i in range(0, len(self.clients)): self.clients[i].seat = i # oya should be always first player # to have compatibility with tenhou format self.set_dealer(0) for client in self.clients: client.player.scores = 25000 self._unique_dealers = 0 self.round_number = 0 def play_game(self): logger.info("The start of the game") is_game_end = False self.init_game() self.replay.init_game(shuffle_seed()) while not is_game_end: self.init_round() results = self.play_round() dealer_won = False was_retake = False for result in results: # we want to increase honba in that case and don't move dealer seat if result["is_abortive_retake"]: dealer_won = True if not result["winner"]: was_retake = True continue if result["winner"].player.is_dealer: dealer_won = True old_dealer = self.dealer # if dealer won we need to increment honba sticks if dealer_won: self.honba_sticks += 1 # otherwise let's move dealer seat else: # retake and dealer is noten if was_retake: self.honba_sticks += 1 else: self.honba_sticks = 0 new_dealer = self._move_position(self.dealer) self.set_dealer(new_dealer) is_game_end = self._check_the_end_of_game(old_dealer) # important increment, we are building wall seed based on the round number self.round_number += 1 winner = self.recalculate_players_position() # winner takes riichi sticks winner.player.scores += self.riichi_sticks * 1000 self.replay.end_game() logger.info("Final Scores: {0}".format( self.players_sorted_by_scores())) total_scores = sum([x.player.scores for x in self.clients]) assert total_scores == 100000, total_scores def init_round(self): """ Generate players hands, dead wall and dora indicators """ self._need_to_check_same_winds = True self.players_with_open_hands = [] self.dora_indicators = [] self.tiles = self._generate_wall() for client in self.clients: client.erase_state() self.dead_wall = self._cut_tiles(14) self.add_new_dora_indicator() for x in range(0, len(self.clients)): client = self.clients[x] # each client think that he is a player with position = 0 # so, we need to move dealer position for each client # and shift scores array client_dealer = self._enemy_position(self.dealer, x) player_scores = deque( [i.player.scores / 100 for i in self.clients]) player_scores.rotate(x * -1) player_scores = list(player_scores) client.table.init_round( self._unique_dealers, self.honba_sticks, self.riichi_sticks, self.dora_indicators[0], client_dealer, player_scores, ) # each player by rotation draw 4 tiles until they have 12 # after this each player draw one more tile # and this is will be their initial hand # we do it to make the tiles allocation in hands # more random for _ in range(0, 3): for client in self.clients: client.player.tiles += self._cut_tiles(4) for client in self.clients: client.player.tiles += self._cut_tiles(1) client.player.tiles = sorted(client.player.tiles) client.player.init_hand(client.player.tiles) logger.info("Seed: {}".format(shuffle_seed())) logger.info("Dealer: {}, {}".format( self.dealer, self.clients[self.dealer].player.name)) logger.info("Wind: {}. Riichi sticks: {}. Honba sticks: {}".format( self._unique_dealers, self.riichi_sticks, self.honba_sticks)) logger.info("Round number: {}".format(self.round_number)) logger.info("Players: {0}".format(self.players_sorted_by_scores())) self.replay.init_round(self.dealer, self._unique_dealers, self.honba_sticks, self.riichi_sticks, self.dora_indicators[0]) def play_round(self) -> []: continue_to_play = True number_of_kan_sets_per_player = {0: 0, 1: 0, 2: 0, 3: 0} while continue_to_play: current_client = self._get_current_client() in_tempai = current_client.player.in_tempai drawn_tile = self._cut_tiles(1)[0] drawn_tile_34 = drawn_tile // 4 current_client.table.count_of_remaining_tiles -= 1 self.replay.draw(current_client.seat, drawn_tile) current_client.player.draw_tile(drawn_tile) tiles = current_client.player.tiles if (self.player_can_call_kyuushu_kyuuhai(current_client.player) and current_client.player.should_call_kyuushu_kyuuhai()): return [self.abortive_retake(AbortiveDraw.NINE_DIFFERENT)] # win by tsumo after tile draw is_win = self.agari.is_agari(TilesConverter.to_34_array(tiles), current_client.player.meld_34_tiles) if is_win: tiles.remove(drawn_tile) can_win = True # with open hand it can be situation when we in the tempai # but our hand doesn't contain any yaku # in that case we can't call ron if not current_client.player.in_riichi: result = current_client.player.ai.estimate_hand_value_or_get_from_cache( drawn_tile_34, is_tsumo=True, is_rinshan=current_client.is_rinshan) can_win = result.error is None if can_win: result = self.process_the_end_of_the_round( tiles=tiles, win_tile=drawn_tile, winner=current_client, loser=None, is_tsumo=True) return [result] else: # we can't win # so let's add tile back to hand # and discard it later tiles.append(drawn_tile) # checks if we can call closed kan or shouminkan current_client_tiles_34 = TilesConverter.to_34_array( current_client.player.tiles) if current_client_tiles_34[drawn_tile_34] == 4 and len( self.tiles) > 1: kan_type = current_client.player.should_call_kan( drawn_tile, open_kan=False, from_riichi=current_client.player.in_riichi) if kan_type: tiles = [ (drawn_tile_34 * 4), (drawn_tile_34 * 4) + 1, (drawn_tile_34 * 4) + 2, (drawn_tile_34 * 4) + 3, ] opened = False if kan_type == MeldPrint.SHOUMINKAN: opened = True meld = MeldPrint( kan_type, tiles, opened=opened, called_tile=drawn_tile, who=current_client.seat, from_who=current_client.seat, ) logger.info("Called meld: {} by {}".format( meld, current_client.player.name)) self.replay.open_meld(meld) if opened: for client in self.clients: client.is_ippatsu = False result = self.check_clients_possible_ron( current_client, drawn_tile, is_tsumogiri=False, is_chankan=True) # the end of the round if result: return result number_of_kan_sets_per_player[current_client.seat] += 1 if (sum(number_of_kan_sets_per_player.values()) == 4 and len(number_of_kan_sets_per_player.values()) != 1): return [self.abortive_retake(AbortiveDraw.FOUR_KANS)] # we need to notify each client about called meld for _client in self.clients: _client.table.add_called_meld( self._enemy_position(current_client.seat, _client.seat), meld) self.add_new_dora_indicator() current_client.is_rinshan = True # after that we will return to the current client next draw continue # we had to clear state after tile draw current_client.is_ippatsu = False current_client.is_rinshan = False # if not in riichi, let's decide what tile to discard if not current_client.player.in_riichi: tile = current_client.player.discard_tile() in_tempai = current_client.player.in_tempai else: tile = current_client.player.discard_tile(drawn_tile, force_tsumogiri=True) who_called_riichi_seat = None if in_tempai and not current_client.player.is_open_hand and current_client.player.can_call_riichi( ): who_called_riichi_seat = current_client.seat for client in self.clients: client.table.add_called_riichi_step_one( self._enemy_position(who_called_riichi_seat, client.seat)) self.replay.riichi(current_client.seat, 1) self.replay.discard(current_client.seat, tile) is_tsumogiri = drawn_tile == tile result = self.check_clients_possible_ron(current_client, tile, is_tsumogiri) # the end of the round if result: # check_clients_possible_ron already returns array return result # if there is no challenger to ron, let's check can we call riichi with tile discard or not if who_called_riichi_seat is not None: self.call_riichi(current_client) for client in self.clients: client.table.add_called_riichi_step_two( self._enemy_position(who_called_riichi_seat, client.seat)) self.replay.riichi(current_client.seat, 2) count_of_riichi_players = 0 for client in self.clients: if client.player.in_riichi: count_of_riichi_players += 1 if count_of_riichi_players == 4: return [self.abortive_retake(AbortiveDraw.FOUR_RIICHI)] # abortive retake result = self._check_same_winds() if result: return [result] # let's check other players hand to possibility open sets possible_melds = [] tile_34 = tile // 4 for other_client in self.clients: # there is no need to check the current client # or check client in riichi if other_client == current_client or other_client.player.in_riichi: continue # was a tile discarded by the left player or not if other_client.seat == 0: is_kamicha_discard = current_client.seat == 3 else: is_kamicha_discard = other_client.seat - current_client.seat == 1 other_client.table.revealed_tiles[tile_34] -= 1 # opened kan other_client_closed_hand_34 = TilesConverter.to_34_array( other_client.player.closed_hand) if (other_client_closed_hand_34[tile_34] == 3 and len(self.tiles) > 1 and other_client.player.should_call_kan( tile, open_kan=True)): tiles = [ (tile_34 * 4), (tile_34 * 4) + 1, (tile_34 * 4) + 2, (tile_34 * 4) + 3, ] meld = MeldPrint( MeldPrint.KAN, tiles, opened=True, called_tile=tile, who=other_client.seat, from_who=current_client.seat, ) logger.info("Called meld: {} by {}".format( meld, other_client.player.name)) self.replay.open_meld(meld) # we changed current client seat self.players_with_open_hands.append(other_client.seat) number_of_kan_sets_per_player[other_client.seat] += 1 if (sum(number_of_kan_sets_per_player.values()) == 4 and len(number_of_kan_sets_per_player.values()) != 1): return [self.abortive_retake(AbortiveDraw.FOUR_KANS)] # we need to notify each client about called meld for _client in self.clients: _client.table.add_called_meld( self._enemy_position(other_client.seat, _client.seat), meld) self.add_new_dora_indicator() # move to draw tile action other_client.is_rinshan = True self.current_client_seat = self._move_position( other_client.seat, shift=-1) continue meld, discard_option = other_client.player.try_to_call_meld( tile, is_kamicha_discard) other_client.table.revealed_tiles[tile_34] += 1 if meld: meld.from_who = current_client.seat meld.who = other_client.seat meld.called_tile = tile possible_melds.append({ "meld": meld, "discard_option": discard_option }) if possible_melds: # pon is more important than chi possible_melds = sorted( possible_melds, key=lambda x: x["meld"].type == Meld.PON) meld = possible_melds[0]["meld"] discard_option = possible_melds[0]["discard_option"] # clear ippatsu after called meld for client_item in self.clients: client_item.is_ippatsu = False # we changed current client with called open set self.current_client_seat = meld.who current_client = self._get_current_client() self.players_with_open_hands.append(self.current_client_seat) logger.info("Called meld: {} by {}".format( meld, current_client.player.name)) # we need to notify each client about called meld for _client in self.clients: _client.table.add_called_meld( self._enemy_position(current_client.seat, _client.seat), meld) self.replay.open_meld(meld) current_client.player.tiles.append(tile) discarded_tile = current_client.player.discard_tile( discard_option) self.replay.discard(current_client.seat, discarded_tile) # the end of the round result = self.check_clients_possible_ron( current_client, discarded_tile, False) if result: # check_clients_possible_ron already returns array return result self.current_client_seat = self._move_position( self.current_client_seat) # retake if not len(self.tiles): continue_to_play = False result = self.process_the_end_of_the_round([], 0, None, None, False) return [result] def check_clients_possible_ron(self, current_client, tile, is_tsumogiri, is_chankan=False) -> []: """ After tile discard let's check all other players can they win or not at this tile """ possible_win_client = [] for other_client in self.clients: # there is no need to check the current client if other_client == current_client: continue # let's store other players discards if not is_chankan: other_client.table.add_discarded_tile( self._enemy_position(current_client.seat, other_client.seat), tile, is_tsumogiri) if self.can_call_ron( other_client, tile, self._enemy_position(current_client.seat, other_client.seat), is_chankan): possible_win_client.append(other_client) if len(possible_win_client) == 3: return [self.abortive_retake(AbortiveDraw.TRIPLE_RON)] # check multiple ron results = [] for client in possible_win_client: result = self.process_the_end_of_the_round( tiles=client.player.tiles, win_tile=tile, winner=client, loser=current_client, is_tsumo=False, is_chankan=is_chankan, ) results.append(result) return results def recalculate_players_position(self): """ For players with same count of scores we need to set position based on their initial seat on the table """ temp_clients = sorted(self.clients, key=lambda x: x.player.scores, reverse=True) for i in range(0, len(temp_clients)): temp_client = temp_clients[i] for client in self.clients: if client.id == temp_client.id: client.player.position = i + 1 # return winner of the game return sorted([x for x in self.clients], key=lambda x: x.player.position)[0] def can_call_ron(self, client, win_tile, shifted_enemy_seat, is_chankan): if not client.player.in_tempai: return False tiles = client.player.tiles is_agari = self.agari.is_agari( TilesConverter.to_34_array(tiles + [win_tile]), client.player.meld_34_tiles) if not is_agari: return False # check for furiten for item in client.player.discards: discarded_tile = item.value // 4 if discarded_tile in client.player.ai.waiting: return False # it can be situation when we are in agari state # but our hand doesn't contain any yaku # in that case we can't call ron if not client.player.in_riichi: result = client.player.ai.estimate_hand_value_or_get_from_cache( win_tile // 4, is_tsumo=False, is_chankan=is_chankan, ) if result.error: return False # bot decided to not call ron if not client.player.should_call_win(win_tile, False, shifted_enemy_seat, is_chankan): return False return True def call_riichi(self, client): client.player.in_riichi = True # -1000 we will deduct in the bot logic self.riichi_sticks += 1 if len(client.player.discards ) == 1 and not self.players_with_open_hands: client.is_daburi = True # we will set it to False after next draw # or called meld client.is_ippatsu = True def set_dealer(self, dealer): self.dealer = dealer self._unique_dealers += 1 for x in range(0, len(self.clients)): client = self.clients[x] # each client think that he is a player with position = 0 # so, we need to move dealer position for each client # and shift scores array client.player.dealer_seat = self._enemy_position(self.dealer, x) # first move should be dealer's move self.current_client_seat = dealer def process_the_end_of_the_round(self, tiles, win_tile, winner, loser, is_tsumo, is_chankan=False): """ Increment a round number and do a scores calculations """ if winner: return self.agari_result(winner, loser, is_tsumo, tiles, win_tile, is_chankan) else: return self.retake() def agari_result(self, winner, loser, is_tsumo, tiles, win_tile, is_chankan): logger.info( "{}: {} + {}".format( is_tsumo and "Tsumo" or "Ron", TilesConverter.to_one_line_string(tiles, print_aka_dora=True), TilesConverter.to_one_line_string([win_tile], print_aka_dora=True), ), ) ura_dora = [] number_of_dora_indicators = len(self.dora_indicators) # add one more dora for riichi win if winner.player.in_riichi: # 9 10 11 12 indices for x in range(number_of_dora_indicators): next_indicator_index = 9 + x ura_dora.append(self.dead_wall[next_indicator_index]) is_tenhou = False # tenhou.net doesn't have renhou is_renhou = False is_chiihou = False if not self.players_with_open_hands and len( winner.player.discards) == 0 and is_tsumo: if winner.player.is_dealer: is_tenhou = True else: is_chiihou = True is_haitei = False is_houtei = False if not self.tiles: if is_tsumo: is_haitei = True else: is_houtei = True config = HandConfig( is_riichi=winner.player.in_riichi, player_wind=winner.player.player_wind, round_wind=winner.player.table.round_wind_tile, is_tsumo=is_tsumo, is_tenhou=is_tenhou, is_renhou=is_renhou, is_chiihou=is_chiihou, is_daburu_riichi=winner.is_daburi, is_ippatsu=winner.is_ippatsu, is_haitei=is_haitei, is_houtei=is_houtei, is_rinshan=winner.is_rinshan, is_chankan=is_chankan, options=OptionalRules( has_aka_dora=settings.FIVE_REDS, has_open_tanyao=settings.OPEN_TANYAO, has_double_yakuman=False, ), ) hand_value = self.finished_hand.estimate_hand_value( tiles=tiles + [win_tile], win_tile=win_tile, melds=winner.player.melds, dora_indicators=self.dora_indicators + ura_dora, config=config, ) if hand_value.error: logger.error("Can't estimate a hand: {}. Error: {}".format( TilesConverter.to_one_line_string(tiles + [win_tile], print_aka_dora=True), hand_value.error)) raise ValueError("Not correct hand") logger.info("Dora indicators: {}".format( TilesConverter.to_one_line_string(self.dora_indicators, print_aka_dora=True))) logger.info("Hand yaku: {}".format(", ".join( str(x) for x in hand_value.yaku))) if loser is not None: loser_seat = loser.seat else: # tsumo loser_seat = winner.seat self.replay.win( winner.seat, loser_seat, win_tile, self.honba_sticks, self.riichi_sticks, hand_value.han, hand_value.fu, hand_value.cost, hand_value.yaku, self.dora_indicators, ura_dora, ) riichi_bonus = self.riichi_sticks * 1000 self.riichi_sticks = 0 honba_bonus = self.honba_sticks * 300 # win by ron if loser: scores_to_pay = hand_value.cost["main"] + honba_bonus win_amount = scores_to_pay + riichi_bonus winner.player.scores += win_amount loser.player.scores -= scores_to_pay logger.info("Win: {0} +{1:,d} +{2:,d}".format( winner.player.name, scores_to_pay, riichi_bonus)) logger.info("Lose: {0} -{1:,d}".format(loser.player.name, scores_to_pay)) # win by tsumo else: calculated_cost = hand_value.cost[ "main"] + hand_value.cost["additional"] * 2 win_amount = calculated_cost + riichi_bonus + honba_bonus winner.player.scores += win_amount logger.info("Win: {0} +{1:,d} +{2:,d}".format( winner.player.name, calculated_cost, riichi_bonus + honba_bonus)) for client in self.clients: if client != winner: if client.player.is_dealer: scores_to_pay = hand_value.cost["main"] else: scores_to_pay = hand_value.cost["additional"] scores_to_pay += honba_bonus / 3 client.player.scores -= scores_to_pay logger.info("Lose: {0} -{1:,d}".format( client.player.name, int(scores_to_pay))) return { "winner": winner, "loser": loser, "is_tsumo": is_tsumo, "is_abortive_retake": False } def retake(self): logger.info("Retake") tempai_users = [] for client in self.clients: if client.player.in_tempai: tempai_users.append(client.seat) tempai_users_count = len(tempai_users) if tempai_users_count == 0 or tempai_users_count == 4: self.honba_sticks += 1 else: # 1 tempai user will get 3000 # 2 tempai users will get 1500 each # 3 tempai users will get 1000 each scores_to_pay = 3000 / tempai_users_count for client in self.clients: if client.player.in_tempai: client.player.scores += scores_to_pay logger.info("{0} +{1:,d}".format(client.player.name, int(scores_to_pay))) # dealer was tempai, we need to add honba stick if client.player.is_dealer: self.honba_sticks += 1 else: client.player.scores -= 3000 / (4 - tempai_users_count) self.replay.retake(tempai_users, self.honba_sticks, self.riichi_sticks) return { "winner": None, "loser": None, "is_tsumo": False, "is_abortive_retake": False } def abortive_retake(self, reason): logger.info("Abortive retake. Reason: {}".format(reason)) self.replay.abortive_retake(reason, self.honba_sticks, self.riichi_sticks) return { "winner": None, "loser": None, "is_tsumo": False, "is_abortive_retake": True } def players_sorted_by_scores(self): return sorted([i.player for i in self.clients], key=lambda x: x.scores, reverse=True) def _check_same_winds(self): if not self._need_to_check_same_winds: return None # with called melds this abortive retake is not possible if self.players_with_open_hands: self._need_to_check_same_winds = False return None # it is possible only for the first 4 discards if len(self.discards) > 4: self._need_to_check_same_winds = False return None # it is too early if len(self.discards) != 4: return None tiles = [x // 4 for x in self.discards] unique_tiles = list(set(tiles)) # first 4 discards wasn't same tiles if len(unique_tiles) != 1: self._need_to_check_same_winds = False return None tile = unique_tiles[1] if tile in WINDS: return self.abortive_retake(AbortiveDraw.SAME_FIRST_WIND) else: self._need_to_check_same_winds = False return None def _check_the_end_of_game(self, dealer_seat): dealer_has_higher_scores = True has_player_with_30000_plus_scores = False dealer = [x for x in self.clients if x.seat == dealer_seat][0] for client in self.clients: # if someone has negative scores at the end of round, we need to end the game if client.player.scores < 0: logger.info("Game end: negative scores") return True if client.player.scores >= 30000: has_player_with_30000_plus_scores = True if client.seat == dealer.seat: continue if client.player.scores > dealer.player.scores: dealer_has_higher_scores = False # orasu ended if self._unique_dealers == 8 and dealer_has_higher_scores: logger.info( "Game end: dealer has higher scores at the end of south wind") return True # we have played all 8 winds (starting from wind 0) # and there is player with 30000+ scores if self._unique_dealers > 7 and has_player_with_30000_plus_scores: logger.info("Game end: 30000+ scores and the end of south wind") return True # west round was finished, we don't want to play north round if self._unique_dealers > 11: logger.info("Game end: the end of west wind") return True return False def _get_current_client(self) -> LocalClient: return self.clients[self.current_client_seat] def _cut_tiles(self, count_of_tiles) -> []: """ Cut the tiles array :param count_of_tiles: how much tiles to cut :return: the array with specified count of tiles """ result = self.tiles[0:count_of_tiles] self.tiles = self.tiles[count_of_tiles:len(self.tiles)] return result def _move_position(self, current_position, shift=1): """ Loop 0 -> 1 -> 2 -> 3 -> 0 """ current_position += shift if current_position > 3: current_position = 0 if current_position < 0: current_position = 3 return current_position def _enemy_position(self, who, from_who): positions = [0, 1, 2, 3] return positions[who - from_who] def _generate_wall(self): # round of played numbers here to be sure that each wall will be unique wall_seed = shuffle_seed() + self.round_number # init seed for random generator seed(wall_seed) wall = [i for i in range(0, 136)] # let's shuffle wall two times just in case wall = self._randomly_shuffle_array(wall) wall = self._randomly_shuffle_array(wall) return wall def _randomly_shuffle_array(self, array): rand_seeds = [randint(0, len(array) - 1) for _ in range(0, len(array))] # for better wall shuffling we had to do it manually # shuffle() didn't make wall to be really random for x in range(0, len(array)): src = x dst = rand_seeds[x] swap = array[x] array[src] = array[dst] array[dst] = swap return array def add_new_dora_indicator(self): number_of_dora_indicators = len(self.dora_indicators) # 2 3 4 5 indices next_indicator_index = 2 + number_of_dora_indicators self.dora_indicators.append(self.dead_wall[next_indicator_index]) if number_of_dora_indicators > 0: self.replay.add_new_dora(self.dora_indicators[-1]) for _client in self.clients: _client.table.add_dora_indicator(self.dora_indicators[-1]) def player_can_call_kyuushu_kyuuhai(self, player): if len(player.discards) > 0 or len(player.melds) > 0: return False tiles_34 = [x // 4 for x in player.tiles] terminals_and_honors = [ x for x in tiles_34 if is_honor(x) or is_terminal(x) ] return len(list(set(terminals_and_honors))) >= 9
from mahjong.hand_calculating.hand import HandCalculator from mahjong.tile import TilesConverter from mahjong.hand_calculating.hand_config import HandConfig from mahjong.meld import Meld calculator = HandCalculator() # we had to use all 14 tiles in that array tiles = TilesConverter.string_to_136_array(man='22444', pin='333567', sou='456') win_tile = TilesConverter.string_to_136_array(sou='4')[0] result = calculator.estimate_hand_value(tiles, win_tile) print(result.han, result.fu) print(result.cost['main']) print(result.yaku) for fu_item in result.fu_details: print(fu_item)
class LogParser: data_to_save = None def __init__(self): self.shanten = Shanten() self.agari = Agari() self.finished_hand = HandCalculator() self.data_to_save = [] self.csv_exporter = CSVExporter() def on_player_draw(self, player, table): pass def on_player_discard(self, player, table, discarded_tile): pass def on_player_tenpai(self, player, table): pass def get_game_rounds(self, log_content, log_id): """ XML parser was really slow here, so I built simple parser to separate log content on tags (grouped by rounds) """ tag_start = 0 rounds = [] tag = None current_tags = [] for x in range(0, len(log_content)): if log_content[x] == ">": tag = log_content[tag_start:x + 1] tag_start = x + 1 # not useful tags skip_tags = ["SHUFFLE", "TAIKYOKU", "mjloggm", "GO", "UN"] if tag and any([x in tag for x in skip_tags]): tag = None # new hand was started if self.is_init_tag(tag) and current_tags: rounds.append(current_tags) current_tags = [] # the end of the game if tag and "owari" in tag: rounds.append(current_tags) if tag: if self.is_init_tag(tag): # we dont need seed information # it appears in old logs format find = re.compile(r'shuffle="[^"]*"') tag = find.sub("", tag) current_tags.append('<LOG_ID id="{}" />'.format(log_id)) # add processed tag to the hand current_tags.append(tag) tag = None return rounds def parse_game_rounds(self, game): self.data_to_save = [] step = 0 for round_item in game: table = Table() log_id = None who_called_meld_on_this_step = None try: for tag in round_item: if self.is_log_id(tag): log_id = self.get_attribute_content(tag, "id") table.log_id = log_id if self.is_init_tag(tag): seed = [ int(x) for x in self.get_attribute_content( tag, "seed").split(",") ] current_hand = seed[0] dora_indicator = seed[5] dealer_seat = int( self.get_attribute_content(tag, "oya")) scores = [ int(x) for x in self.get_attribute_content( tag, "ten").split(",") ] table.init(dealer_seat, current_hand, dora_indicator, step, scores) table.get_player(0).init_hand( self.get_attribute_content(tag, "hai0")) table.get_player(1).init_hand( self.get_attribute_content(tag, "hai1")) table.get_player(2).init_hand( self.get_attribute_content(tag, "hai2")) table.get_player(3).init_hand( self.get_attribute_content(tag, "hai3")) step += 1 if self.is_draw(tag): tile = self.parse_tile(tag) player_seat = self.get_player_seat(tag) player = table.get_player(player_seat) player.draw_tile(tile) self.on_player_draw(player, table) if self.is_discard(tag): tile = self.parse_tile(tag) player_seat = self.get_player_seat(tag) player = table.get_player(player_seat) is_tsumogiri = tile == player.last_drawn_tile after_meld = player_seat == who_called_meld_on_this_step discard = Discard(tile, is_tsumogiri, after_meld, False) player.discard_tile(discard) tenpai_after_discard = False tiles_34 = TilesConverter.to_34_array(player.tiles) melds_34 = player.melds_34 if self.shanten.calculate_shanten(tiles_34, melds_34) == 0: tenpai_after_discard = True self.on_player_tenpai(player, table) else: player.in_tempai = False player.discards[ -1].tenpai_after_discard = tenpai_after_discard who_called_meld_on_this_step = None self.on_player_discard(player, table, tile) if self.is_meld_set(tag): meld = self.parse_meld(tag) player = table.get_player(meld.who) # when we called chankan we need to remove pon set from hand if meld.type == ParserMeld.CHANKAN: player.tiles.remove(meld.called_tile) pon_set = [ x for x in player.melds if x.tiles[0] == meld.tiles[0] ][0] player.melds.remove(pon_set) player.add_meld(meld) # if it was not kan/chankan let's add it to the hand if meld.type != ParserMeld.CHANKAN and meld.type != ParserMeld.KAN: player.tiles.append(meld.called_tile) # indication that tile was taken from discard if meld.opened: for meld_player in table.players: if meld_player.discards and meld_player.discards[ -1].tile == meld.called_tile: meld_player.discards[ -1].was_given_for_meld = True # for closed kan we had to remove tile from hand if (meld.type == ParserMeld.KAN and not meld.opened and meld.called_tile in player.tiles): player.tiles.remove(meld.called_tile) who_called_meld_on_this_step = meld.who if self.is_riichi(tag): riichi_step = int( self.get_attribute_content(tag, "step")) who = int(self.get_attribute_content(tag, "who")) player = table.get_player(who) if riichi_step == 1: player.in_riichi = True if riichi_step == 2: player.discards[-1].after_riichi = True if self.is_new_dora(tag): dora = int(self.get_attribute_content(tag, "hai")) table.add_dora(dora) except Exception as e: logger.error("Failed to process log: {}".format(log_id)) logger.error(e, exc_info=True) return self.data_to_save def get_player_waiting(self, player): tiles = player.closed_hand if len(tiles) == 1: return [tiles[0] // 4] tiles_34 = TilesConverter.to_34_array(tiles) waiting = [] for j in range(0, 34): # we already have 4 tiles in hand # and we can't wait on 5th if tiles_34[j] == 4: continue tiles_34[j] += 1 if self.agari.is_agari(tiles_34): waiting.append(j) tiles_34[j] -= 1 return waiting def calculate_waiting_costs(self, player, player_waiting): waiting = [] for tile in player_waiting: config = HandConfig( is_riichi=player.discards[-1].after_riichi, player_wind=player.player_wind, round_wind=player.table.round_wind, options=OptionalRules(has_aka_dora=True, has_open_tanyao=True), ) win_tile = tile * 4 # we don't need to think, that our waiting is aka dora if win_tile in AKA_DORA_LIST: win_tile += 1 tiles = player.tiles + [win_tile] result = self.finished_hand.estimate_hand_value( tiles, win_tile, player.melds, player.table.dora_indicators, config) if result.error: waiting.append({ "tile": win_tile, "han": None, "fu": None, "cost": 0, "yaku": [] }) else: waiting.append({ "tile": win_tile, "han": result.han, "fu": result.fu, "cost": result.cost["main"], "yaku": [{ "id": x.yaku_id, "name": x.name } for x in result.yaku], }) return waiting def get_attribute_content(self, tag, attribute_name): result = re.findall(r'{}="([^"]*)"'.format(attribute_name), tag) return result and result[0] or None def is_discard(self, tag): skip_tags = ["<GO", "<FURITEN", "<DORA"] if any([x in tag for x in skip_tags]): return False match_discard = re.match(r"^<[defgDEFG]+\d*", tag) if match_discard: return True return False def is_draw(self, tag): match_discard = re.match(r"^<[tuvwTUVW]+\d*", tag) if match_discard: return True return False def parse_tile(self, message): result = re.match(r"^<[defgtuvwDEFGTUVW]+\d*", message).group() return int(result[2:]) def get_player_seat(self, tag): player_sign = tag.lower()[1] if player_sign == "d" or player_sign == "t": player_seat = 0 elif player_sign == "e" or player_sign == "u": player_seat = 1 elif player_sign == "f" or player_sign == "v": player_seat = 2 else: player_seat = 3 return player_seat def parse_meld(self, tag): who = int(self.get_attribute_content(tag, "who")) data = int(self.get_attribute_content(tag, "m")) meld = ParserMeld() meld.who = who meld.from_who = ((data & 0x3) + meld.who) % 4 if data & 0x4: self.parse_chi(data, meld) elif data & 0x18: self.parse_pon(data, meld) elif data & 0x20: # nuki pass else: self.parse_kan(data, meld) return meld def parse_chi(self, data, meld): meld.type = ParserMeld.CHI t0, t1, t2 = (data >> 3) & 0x3, (data >> 5) & 0x3, (data >> 7) & 0x3 base_and_called = data >> 10 base = base_and_called // 3 called = base_and_called % 3 base = (base // 7) * 9 + base % 7 meld.tiles = [ t0 + 4 * (base + 0), t1 + 4 * (base + 1), t2 + 4 * (base + 2) ] meld.called_tile = meld.tiles[called] def parse_pon(self, data, meld): t4 = (data >> 5) & 0x3 t0, t1, t2 = ((1, 2, 3), (0, 2, 3), (0, 1, 3), (0, 1, 2))[t4] base_and_called = data >> 9 base = base_and_called // 3 called = base_and_called % 3 if data & 0x8: meld.type = ParserMeld.PON meld.tiles = [t0 + 4 * base, t1 + 4 * base, t2 + 4 * base] meld.called_tile = meld.tiles[called] else: meld.type = ParserMeld.CHANKAN meld.tiles = [ t0 + 4 * base, t1 + 4 * base, t2 + 4 * base, t4 + 4 * base ] meld.called_tile = meld.tiles[3] def parse_kan(self, data, meld): base_and_called = data >> 8 base = base_and_called // 4 meld.type = ParserMeld.KAN meld.tiles = [4 * base, 1 + 4 * base, 2 + 4 * base, 3 + 4 * base] called = base_and_called % 4 meld.called_tile = meld.tiles[called] # to mark closed\opened kans meld.opened = meld.who != meld.from_who def is_init_tag(self, tag): return tag and "INIT" in tag def is_redraw_tag(self, tag): return tag and "RYUUKYOKU" in tag def is_agari_tag(self, tag): return tag and "AGARI" in tag def is_log_id(self, tag): return tag and "LOG_ID" in tag def is_meld_set(self, tag): return tag and "<N who=" in tag def is_riichi(self, tag): return tag and "REACH " in tag def is_new_dora(self, tag): return tag and "<DORA" in tag
def tsumo(self, draw, game): if draw in winning_tiles(str(self.hand)): calculator = HandCalculator() dora = str(game.dora) if self.in_riichi: dora += str(game.ura_dora) dora_indicators = TilesConverter.one_line_string_to_136_array( dora, has_aka_dora=True) haitei = False tenhou = False chiihou = False if game.wall.remaining == 0: haitei = True if game.tenhou and game.wall.remaining > 65: if self.seat == EAST: tenhou = True else: chiihou = True config = HandConfig(options = OptionalRules(has_open_tanyao = True, has_aka_dora = True),\ is_tsumo = True,\ is_riichi = self.in_riichi,\ is_ippatsu = self.ippatsu,\ is_rinshan = self.rinshan,\ is_haitei = haitei,\ is_daburu_riichi = self.double_riichi,\ is_tenhou = tenhou,\ is_chiihou = chiihou,\ player_wind = self.seat,\ round_wind = game.round_wind\ ) melds = [] meld_string = '' for meld in self.melds.melds: opened = True if len(meld) == 4: meld_type = mjMeld.KAN opened = meld.opened elif meld.tiles.count(meld.tiles[0]) == 3: meld_type = mjMeld.PON else: meld_type = mjMeld.CHI #not sure if the meld_type is necessary, but since it's not a hassle i'll leave it in new_meld = mjMeld(meld_type = meld_type,\ tiles = TilesConverter.one_line_string_to_136_array(str(meld), has_aka_dora = True),\ opened = opened) melds.append(new_meld) #really dumb workaround because the hand is supposed to be exactly 14 tiles #and adding the kan as a string directly to the rest of the hand #causes the hand to exceed 14 tiles #this would subtract by 4 if len(meld) referred to the length of the #string associated with the meld, but len(meld) actually refers #to the number of tiles in the meld (aka it ignores the suit) #so the length of '222p' is 3 and the length of '1111m' is 4 meld_string += str(meld)[len(meld) - 3:] hand = TilesConverter.one_line_string_to_136_array( str(self.hand) + str(draw) + str(meld_string), has_aka_dora=True) draw = TilesConverter.one_line_string_to_136_array( str(draw), has_aka_dora=True)[0] result = calculator.estimate_hand_value(hand, draw, melds = melds, \ dora_indicators = dora_indicators, config = config) if result.yaku and result.cost and not result.error: return result else: return False else: return False
from mahjong.tile import TilesConverter from mahjong.hand_calculating.hand import HandCalculator hc = HandCalculator() tc = TilesConverter() tiles = tc.string_to_136_array('22444', '333567', '444') result = hc.estimate_hand_value(tiles, tiles[0], None, None, None) print(result)
class ImplementationAI(InterfaceAI): version = '0.3.2' agari = None shanten = None defence = None hand_divider = None finished_hand = None last_discard_option = None previous_shanten = 7 in_defence = False waiting = None current_strategy = None def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() self.shanten = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.previous_shanten = 7 self.current_strategy = None self.waiting = [] self.in_defence = False self.last_discard_option = None # Added for cowboy self.wanted_tiles_count = 0 self.pushing = False def init_hand(self): """ Let's decide what we will do with our hand (like open for tanyao and etc.) """ self.determine_strategy() def erase_state(self): self.current_strategy = None self.in_defence = False self.last_discard_option = None # Added for cowboy self.previous_shanten = 7 self.pushing = False def draw_tile(self, tile): """ :param tile: 136 tile format :return: """ self.determine_strategy() def discard_tile(self, discard_tile): # we called meld and we had discard tile that we wanted to discard if discard_tile is not None: if not self.last_discard_option: return discard_tile return self.process_discard_option(self.last_discard_option, self.player.closed_hand, True) results, shanten = self.calculate_outs(self.player.tiles, self.player.closed_hand, self.player.open_hand_34_tiles) if shanten < self.previous_shanten: logger.info("Shanten: {}".format(shanten)) self.previous_shanten = shanten selected_tile = self.process_discard_options_and_select_tile_to_discard(results, shanten) # bot think that there is a threat on the table # and better to fold # if we can't find safe tiles, let's continue to build our hand if self.defence.should_go_to_defence_mode(selected_tile): if not self.in_defence: logger.info('We decided to fold against other players') self.in_defence = True self.player.set_state("DEFENCE") else: #logger.info("Player is alreay in defence") pass defence_results, shanten = self.calculate_outs(self.player.tiles, self.player.closed_hand, self.player.open_hand_34_tiles) defence_tile = self.defence.try_to_find_safe_tile_to_discard(defence_results) if defence_tile: return self.process_discard_option(defence_tile, self.player.closed_hand) else: self.in_defence = False # Process the discard option before changing the state card2discard = self.process_discard_option(selected_tile, self.player.closed_hand) # After adjusting the defence, time to update the state if shanten == 0 and self.player.play_state == "PREPARING" and results: # and results for debugging if self.wanted_tiles_count > 4: self.player.set_state("PROACTIVE_GOODSHAPE") else: self.player.set_state("PROACTIVE_BADSHAPE") return card2discard def process_discard_options_and_select_tile_to_discard(self, results, shanten, had_was_open=False): tiles_34 = TilesConverter.to_34_array(self.player.tiles) # we had to update tiles value there # because it is related with shanten number for result in results: result.tiles_count = self.count_tiles(result.waiting, tiles_34) result.calculate_value(shanten) # current strategy can affect on our discard options # so, don't use strategy specific choices for calling riichi if self.current_strategy: results = self.current_strategy.determine_what_to_discard(self.player.closed_hand, results, shanten, False, None, had_was_open) return self.chose_tile_to_discard(results) 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 count_tiles(self, waiting, tiles_34): n = 0 for item in waiting: n += 4 - self.player.total_tiles(item, tiles_34) return n def try_to_call_meld(self, tile, is_kamicha_discard): if not self.current_strategy: return None, None meld, discard_option = self.current_strategy.try_to_call_meld(tile, is_kamicha_discard) tile_to_discard = None if discard_option: self.last_discard_option = discard_option tile_to_discard = discard_option.tile_to_discard return meld, tile_to_discard def determine_strategy(self): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False old_strategy = self.current_strategy self.current_strategy = None # order is important strategies = [ YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player), HonitsuStrategy(BaseStrategy.HONITSU, self.player), ] if self.player.table.has_open_tanyao: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) for strategy in strategies: if strategy.should_activate_strategy(): self.current_strategy = strategy if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: message = '{} switched to {} strategy'.format(self.player.name, self.current_strategy) if old_strategy: message += ' from {}'.format(old_strategy) logger.info(message) logger.info('With such a hand: {}'.format(TilesConverter.to_one_line_string(self.player.tiles))) if not self.current_strategy and old_strategy: logger.debug('{} gave up on {}'.format(self.player.name, old_strategy)) return self.current_strategy and True or False def chose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: """ Try to find best tile to discard, based on different valuations """ def sorting(x): # - is important for x.tiles_count # in that case we will discard tile that will give for us more tiles # to complete a hand return x.shanten, -x.tiles_count, x.valuation # util for drawing def get_order(t): # if it is honor if t // 9 >= 3: return 0 else: return min((t % 9), (8 - (t % 9))) + 1 def display_waiting(w): return TilesConverter.to_one_line_string([t * 4 for t in w]) had_to_be_discarded_tiles = [x for x in results if x.had_to_be_discarded] if had_to_be_discarded_tiles: had_to_be_discarded_tiles = sorted(had_to_be_discarded_tiles, key=sorting) selected_tile = had_to_be_discarded_tiles[0] else: results = sorted(results, key=sorting) #print("Len: ", len(results)) # init the temp_tile temp_tile = results[0] # remove needed tiles from discard options results = [x for x in results if not x.had_to_be_saved] # let's chose most valuable tile first if results: temp_tile = results[0] else: return temp_tile # and let's find all tiles with same shanten results_with_same_shanten = [x for x in results if x.shanten == temp_tile.shanten] # if there are 7 pairs tiles_34 = TilesConverter.to_34_array(self.player.tiles) paired_tiles = [x for x in range(0, 34) if tiles_34[x] == 2] num_pairs = len(paired_tiles) if num_pairs == 4 and temp_tile.shanten > 1 and not self.player.in_seven_pairs and not self.player.params.get("fool_in_pairs"): logger.info("There are 4 pairs!") if len(self.player.discards) > 6: logger.info("However it's too late for seven pairs.") for r in results: if r.tile_to_discard in paired_tiles: logger.info("With hand: {}".format(TilesConverter.to_one_line_string(self.player.tiles))) logger.info("Discard {}".format(display_waiting([r.tile_to_discard]))) return r else: logger.info("It's early, okay to go with seven pairs.") self.player.in_seven_pairs = True # TODO: a smart seven pairs strategy should be carried if self.player.in_seven_pairs and not self.player.params.get("fool_in_pairs"): single_tiles = [x for x in range(0,34) if tiles_34[x] in [1,3,4]] single_tiles.sort(key=lambda x: (self.count_tiles([x], tiles_34) >= 2, -get_order(x))) for s in single_tiles: # actually only #1 would be used most of the time for r in results: if r.tile_to_discard == s: logger.info("SevenPairsStrategy:") logger.info("Hand: {}".format(TilesConverter.to_one_line_string(self.player.tiles))) logger.info("Discard: {}".format(display_waiting([s]))) return r # if in drawing if temp_tile.shanten == 0: print("It's a drawing hand!") print("Hand: {}".format(TilesConverter.to_one_line_string(self.player.tiles))) # assume that temp tile got the biggest waiting if temp_tile.tiles_count > 4: print("It's a good shape, go for it.") else: logger.info("It's a bad shape drawing hand, need some calculation.") logger.info("Possible choices: {}".format(TilesConverter.to_one_line_string([x.tile_to_discard*4 for x in results_with_same_shanten]))) possible_choices = [(temp_tile, 99)] for r in results_with_same_shanten: print("\nCut:", display_waiting([r.tile_to_discard])) print("Waiting:", display_waiting(r.waiting)) print("Order:", [get_order(t) for t in r.waiting]) print("Outs:", r.tiles_count) if r.tiles_count == 0: print("It's an impossible drawing.") continue if len(r.waiting) == 1: print("It's an 1 out drawing.") possible_choices.append((r, get_order(r.waiting[0]))) else: print("It's a multiple out drawing.") r.waiting.sort(key=get_order) possible_choices.append((r, get_order(r.waiting[0]))) possible_choices.sort(key=lambda x: (x[1], -x[0].tiles_count)) final_choice = possible_choices[0][0] logger.info("Choice: {} {} {}".format(display_waiting([final_choice.tile_to_discard]), "with waiting", display_waiting(final_choice.waiting))) return final_choice # if not in drawing or in drawing with good shape possible_options = [temp_tile] for discard_option in results_with_same_shanten: # there is no sense to check already chosen tile if discard_option.tile_to_discard == temp_tile.tile_to_discard: continue # we don't need to select tiles almost dead waits if discard_option.tiles_count <= 2: continue # let's check all other tiles with same shanten # maybe we can find tiles that have almost same tiles count number # Cowboy: +-2 is a big difference, but +-1 is not diff = 1 if self.player.params.get("big_diff"): diff = 2 if temp_tile.tiles_count - diff < discard_option.tiles_count < temp_tile.tiles_count + diff: possible_options.append(discard_option) # let's sort got tiles by value and let's chose less valuable tile to discard possible_options = sorted(possible_options, key=lambda x: x.valuation) selected_tile = possible_options[0] if selected_tile.shanten == 0: print("\nChoice:", display_waiting([selected_tile.tile_to_discard]), "with waiting", display_waiting(selected_tile.waiting)) return selected_tile def process_discard_option(self, discard_option, closed_hand, force_discard=False): self.waiting = discard_option.waiting self.wanted_tiles_count = discard_option.tiles_count self.player.ai.previous_shanten = discard_option.shanten self.player.in_tempai = self.player.ai.previous_shanten == 0 # when we called meld we don't need "smart" discard if force_discard: return discard_option.find_tile_in_hand(closed_hand) last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard: return self.player.last_draw else: return discard_option.find_tile_in_hand(closed_hand) def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): """ :param win_tile: 34 tile format :param tiles: :param call_riichi: :return: """ win_tile *= 4 # we don't need to think, that our waiting is aka dora if win_tile in AKA_DORA_LIST: win_tile += 1 if not tiles: tiles = self.player.tiles tiles += [win_tile] config = HandConfig( is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind, has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao ) result = self.finished_hand.estimate_hand_value(tiles, win_tile, self.player.melds, self.player.table.dora_indicators, config) return result def should_call_riichi(self): logger.info("Can call a reach!") # empty waiting can be found in some cases if not self.waiting: logger.info("However it is impossible to win.") return False # In pushing state, it's better to call it if self.pushing: logger.info("Go for it! The player is in pushing state.") return True # Get the rank EV after round 3 if self.table.round_number >= 5: # DEBUG: set this to 0 try: possible_hand_values = [self.estimate_hand_value(tile, call_riichi=True).cost["main"] for tile in self.waiting] except Exception as e: print(e) possible_hand_values = [2000] hand_value = sum(possible_hand_values) / len(possible_hand_values) hand_value += self.table.count_of_riichi_sticks * 1000 if self.player.is_dealer: hand_value += 700 # EV for dealer combo lose_estimation = 6000 if self.player.is_dealer else 7000 hand_shape = "pro_bad_shape" if self.wanted_tiles_count <= 4 else "pro_good_shape" rank_ev = self.defence.get_rank_ev(hand_value, lose_estimation, COUNTER_RATIO[hand_shape][len(self.player.discards)]) logger.info('''Cowboy: Proactive reach: Hand value: {} Hand shape: {} Is dealer: {} Current ranking: {} '''.format(hand_value, hand_shape, self.player.is_dealer, self.table.get_players_sorted_by_scores())) logger.info("Rank EV for proactive reach: {}".format(rank_ev)) if rank_ev < 0: logger.info("It's better to fold.") return False else: logger.info("Go for it!") return True should_attack = not self.defence.should_go_to_defence_mode() # For bad shape, at least 1 dora is required # Get count of dora dora_count = sum([plus_dora(x, self.player.table.dora_indicators) for x in self.player.tiles]) # aka dora dora_count += sum([1 for x in self.player.tiles if is_aka_dora(x, self.player.table.has_open_tanyao)]) if self.wanted_tiles_count <= 4 and dora_count == 0 and not self.player.is_dealer: should_attack = False logger.info("A bad shape with no dora, don't call it.") # # If player is on the top, no need to call reach # if self.player == self.player.table.get_players_sorted_by_scores()[0] and self.player.scores > 30000: # should_attack = False # logger.info("Player is in 1st position, no need to call reach.") if should_attack: # If we are proactive, let's set the state! logger.info("Go for it!") if self.player.play_state == "PREPARING": # If not changed in defense actions if self.wanted_tiles_count > 4: self.player.set_state("PROACTIVE_GOODSHAPE") else: self.player.set_state("PROACTIVE_BADSHAPE") return True else: logger.info("However it's better to fold.") return False # These codes are unreachable, it is fine. waiting = self.waiting[0] tiles = self.player.closed_hand + [waiting * 4] closed_melds = [x for x in self.player.melds if not x.opened] for meld in closed_melds: tiles.extend(meld.tiles[:3]) tiles_34 = TilesConverter.to_34_array(tiles) results = self.hand_divider.divide_hand(tiles_34) result = results[0] count_of_pairs = len([x for x in result if is_pair(x)]) # with chitoitsu we can call a riichi with pair wait if count_of_pairs == 7: return True for hand_set in result: # better to not call a riichi for a pair wait # it can be easily improved if is_pair(hand_set) and waiting in hand_set: return False return True def should_call_kan(self, tile, open_kan): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :return: kan type """ # 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) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # let's check can we upgrade opened pon to the kan if pon_melds: for meld in pon_melds: # tile is equal to our already opened pon, # so let's call chankan! 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 # we have 3 tiles in our hand, # so we can try to call closed meld 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) # called kan will not ruin our hand if new_shanten <= previous_shanten: return Meld.KAN return None def should_call_win(self, tile, enemy_seat): return True def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ #if self.defence.should_go_to_defence_mode(): # self.in_defence = True # No need to check it here pass @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:]
def erase_state(self): self.hand_cache_shanten = {} self.hand_cache_estimation = {} self.finished_hand = HandCalculator() self.grp_features = []
import time from game import Hand from game import Wall from mahjong.hand_calculating.hand import HandCalculator, HandConfig from mahjong.shanten import Shanten from mahjong.tile import TilesConverter from mahjong.agari import Agari from mahjong.meld import Meld import numpy as np c = HandCalculator() st = Shanten() ag = Agari() ops = 1e7 # Control Code k = 0 cstart = time.time_ns() while k < ops: k += 1 w = Wall() hand = Hand(w) hand.draw(w) cend = time.time_ns() # Test Code k = 0 start = time.time_ns() while k < ops:
def win(): req = flask.request.get_json() calculator = HandCalculator() try: tiles = TilesConverter.one_line_string_to_136_array(req["hands"], has_aka_dora=True) except KeyError: return flask.jsonify({"error": "hands required"}), 400 except ValueError: return flask.jsonify({"error": "invalid hands"}), 400 try: win_tile = TilesConverter.one_line_string_to_136_array( req["win_tile"], has_aka_dora=True)[0] except KeyError: return flask.jsonify({"error": "win_tile required"}), 400 except ValueError: return flask.jsonify({"error": "invalid win_tile"}), 400 melds = [] try: melds = req["melds"] except KeyError: pass meld_tiles = [] for meld in melds: try: m = meld["meld"] except KeyError: return flask.jsonify({"error": "meld required"}), 400 try: if m == "chi": meld_tiles.append( Meld( Meld.CHI, TilesConverter.one_line_string_to_136_array( meld["tiles"]))) elif m == "pon": meld_tiles.append( Meld( Meld.PON, TilesConverter.one_line_string_to_136_array( meld["tiles"]))) elif m == "minkan": meld_tiles.append( Meld( Meld.KAN, TilesConverter.one_line_string_to_136_array( meld["tiles"]), True)) elif m == "ankan": meld_tiles.append( Meld( Meld.KAN, TilesConverter.one_line_string_to_136_array( meld["tiles"]), False)) elif m == "kakan": meld_tiles.append( Meld( Meld.CHANKAN, TilesConverter.one_line_string_to_136_array( meld["tiles"]))) else: return flask.jsonify({"error": "invalid meld"}), 400 except KeyError: return flask.jsonify({"error": "meld tiles required"}), 400 except ValueError: return flask.jsonify({"error": "invalid meld tiles"}), 400 try: tsumo = req["tsumo"] except KeyError: tsumo = False try: riichi = req["riichi"] except KeyError: riichi = False try: ippatsu = req["ippatsu"] except KeyError: ippatsu = False try: rinshan = req["rinshan"] except KeyError: rinshan = False try: chankan = req["chankan"] except KeyError: chankan = False try: haitei = req["haitei"] except KeyError: haitei = False try: houtei = req["houtei"] except KeyError: houtei = False try: double_riichi = req["double_riichi"] except KeyError: double_riichi = False try: tenhou = req["tenhou"] except KeyError: tenhou = False try: chiihou = req["chiihou"] except KeyError: chiihou = False try: wind_player = req["wind_player"] if wind_player == "east": wind_player = EAST elif wind_player == "south": wind_player = SOUTH elif wind_player == "west": wind_player = WEST elif wind_player == "north": wind_player = NORTH else: return flask.jsonify({"error": "invalid wind_player"}), 400 except KeyError: return flask.jsonify({"error": "wind_player required"}), 400 try: wind_round = req["wind_round"] if wind_round == "east": wind_round = EAST elif wind_round == "south": wind_round = SOUTH elif wind_round == "west": wind_round = WEST elif wind_round == "north": wind_round = NORTH else: return flask.jsonify({"error": "invalid wind_round"}), 400 except KeyError: return flask.jsonify({"error": "wind_round required"}), 400 try: dora_indicators = TilesConverter.one_line_string_to_136_array( req["dora_indicators"], has_aka_dora=True) except KeyError: return flask.jsonify({"error": "dora_indicators required"}), 400 except ValueError: return flask.jsonify({"error": "invalid dora_indicators"}), 400 option = OptionalRules(has_open_tanyao=True, has_aka_dora=True, has_double_yakuman=False) config = HandConfig(is_tsumo=tsumo, is_riichi=riichi, is_ippatsu=ippatsu, is_rinshan=rinshan, is_chankan=chankan, is_haitei=haitei, is_houtei=houtei, is_daburu_riichi=double_riichi, is_tenhou=tenhou, is_chiihou=chiihou, player_wind=wind_player, round_wind=wind_round, options=option) result = calculator.estimate_hand_value(tiles, win_tile, dora_indicators=dora_indicators, config=config) if result.error: return flask.jsonify({"error": result.error}), 400 yaku = map(lambda y: y.japanese, result.yaku) return flask.jsonify({ "cost": result.cost, "han": result.han, "fu": result.fu, "yaku": list(yaku), "error": None })
from mahjong.tile import TilesConverter from mahjong.hand_calculating.hand_config import HandConfig, OptionalRules from mahjong.meld import Meld from mahjong.constants import EAST, SOUTH, WEST, NORTH def print_hand_result(hand_result): print(hand_result.han, hand_result.fu) print(hand_result.cost['main'], result.cost['additional']) print(hand_result.yaku) for fu_item in hand_result.fu_details: print(fu_item) print('') calculator = HandCalculator() class ClassCheckMahjongtiles: def __init__(self): self.Img = '5.png' self.ManzuInHandtile = [] self.SozuInHandtile = [] self.PinzuInHandtile = [] self.ZihaiInHandtile = [] self.ManzuTemplates = [['1', 'Templates/Manzu/p_ms1_1.png'], ['2', 'Templates/Manzu/p_ms2_1.png'], ['3', 'Templates/Manzu/p_ms3_1.png'], ['4', 'Templates/Manzu/p_ms4_1.png'], ['5', 'Templates/Manzu/p_ms5_1.png'],
class FeatureGenerator: def __init__(self): """ changed the input from filename to tiles_state_and_action data By Jun Lin """ self.shanten_calculator = Shanten() self.hc = HandCalculator() self.sc = ScoresCalculator() self.hand_cache_shanten = {} self.hand_cache_points = {} def build_cache_key(self, tiles_34): return hashlib.md5(marshal.dumps(tiles_34)).hexdigest() def calculate_shanten_or_get_from_cache(self, closed_hand_34): key = self.build_cache_key(closed_hand_34) if key in self.hand_cache_shanten: return self.hand_cache_shanten[key] result = self.shanten_calculator.calculate_shanten(closed_hand_34) self.hand_cache_shanten[key] = result return result def calculate_ponits_or_get_from_cache(self, closed_left_tiles_34, win_tile, melds, dora_indicators): tiles_34 = closed_left_tiles_34[:] for meld in melds: for x in meld.tiles_34: tiles_34[x] += 1 key = self.build_cache_key(tiles_34 + [win_tile] + dora_indicators) if key in self.hand_cache_points: return self.hand_cache_points[key] # print(closed_left_tiles_34) # print(melds) hc_result = self.hc.estimate_hand_value( TilesConverter.to_136_array(tiles_34), win_tile, melds, dora_indicators) sc_result = self.sc.calculate_scores(hc_result.han, hc_result.fu, HandConfig(HandConstants())) result = sc_result["main"] self.hand_cache_points[key] = result return result def canwinbyreplace(self, closed_left_tiles_34, melds, dora_indicators, tiles_could_draw, replacelimit): def _draw(closed_left_tiles_34, melds, dora_indicators, tiles_could_draw, replacelimit): if self.calculate_shanten_or_get_from_cache( closed_left_tiles_34) > replacelimit: return 0 result = 0 if replacelimit == 0: for idx in range(34): if tiles_could_draw[idx] > 0: closed_left_tiles_34[idx] += 1 if self.calculate_shanten_or_get_from_cache( closed_left_tiles_34) == -1: ponits = self.calculate_ponits_or_get_from_cache( closed_left_tiles_34, idx * 4, melds, dora_indicators) result = max(result, ponits) closed_left_tiles_34[idx] -= 1 else: for idx, count in enumerate(tiles_could_draw): if count > 0: tiles_could_draw[idx] -= 1 closed_left_tiles_34[idx] += 1 ponits = _discard(closed_left_tiles_34, melds, dora_indicators, tiles_could_draw, replacelimit) result = max(result, ponits) closed_left_tiles_34[idx] -= 1 tiles_could_draw[idx] += 1 return result def _discard(closed_left_tiles_34, melds, dora_indicators, tiles_could_draw, replacelimit): result = 0 for idx, count in enumerate(closed_left_tiles_34): if count > 0: closed_left_tiles_34[idx] -= 1 replacelimit -= 1 ponits = _draw(closed_left_tiles_34, melds, dora_indicators, tiles_could_draw, replacelimit) result = max(result, ponits) replacelimit += 1 closed_left_tiles_34[idx] += 1 return result return _draw(closed_left_tiles_34, melds, dora_indicators, tiles_could_draw, replacelimit) def open_hands_detail_to_melds(self, open_hands_detail): melds = [] for ohd in open_hands_detail: tiles = ohd["tiles"] if ohd["meld_type"] == "Pon": meld_type = "pon" opened = True elif ohd["meld_type"] == "Chi": meld_type = "chi" opened = True elif ohd["meld_type"] == "AnKan": meld_type = "kan" opened = False else: meld_type = "kan" opened = True meld = Meld(meld_type, tiles, opened) melds.append(meld) return melds def getPlayerTiles(self, player_tiles): closed_hand_136 = player_tiles.get('closed_hand:', []) open_hand_136 = player_tiles.get('open_hand', []) discarded_tiles_136 = player_tiles.get('discarded_tiles', []) closed_hand_feature = np.zeros((4, 34)) open_hand_feature = np.zeros((4, 34)) discarded_tiles_feature = np.zeros((4, 34)) for val in closed_hand_136: idx = 0 while (closed_hand_feature[idx][val // 4] == 1): idx += 1 closed_hand_feature[idx][val // 4] = 1 for val in open_hand_136: idx = 0 while (open_hand_feature[idx][val // 4] == 1): idx += 1 open_hand_feature[idx][val // 4] = 1 for val in discarded_tiles_136: idx = 0 while (discarded_tiles_feature[idx][val // 4] == 1): idx += 1 discarded_tiles_feature[idx][val // 4] = 1 return np.concatenate( (closed_hand_feature, open_hand_feature, discarded_tiles_feature)) def getSelfTiles(self, tiles_state_and_action): player_tiles = tiles_state_and_action["player_tiles"] return self.getPlayerTiles(player_tiles) def getEnemiesTiles(self, tiles_state_and_action): player_seat = tiles_state_and_action["player_id"] enemies_tiles_list = tiles_state_and_action["enemies_tiles"] if (len(enemies_tiles_list) == 3): enemies_tiles_list.insert(player_seat, tiles_state_and_action["player_tiles"]) enemies_tiles_feature = np.empty((0, 34)) for i in range(3): player_seat = (player_seat + 1) % 4 player_tiles = self.getPlayerTiles(enemies_tiles_list[player_seat]) enemies_tiles_feature = np.concatenate( (enemies_tiles_feature, player_tiles)) return enemies_tiles_feature def getDoraIndicatorList(self, tiles_state_and_action): dora_indicator_list = tiles_state_and_action["dora"] dora_indicator_feature = np.zeros((5, 34)) for idx, val in enumerate(dora_indicator_list): dora_indicator_feature[idx][val // 4] = 1 return dora_indicator_feature def getDoraList(self, tiles_state_and_action): def indicator2dora(dora_indicator): dora = dora_indicator // 4 if dora < 27: # EAST if dora == 8: dora = -1 elif dora == 17: dora = 8 elif dora == 26: dora = 17 else: dora -= 9 * 3 if dora == 3: dora = -1 elif dora == 6: dora = 3 dora += 9 * 3 dora += 1 return dora dora_indicator_list = tiles_state_and_action["dora"] dora_feature = np.zeros((5, 34)) for idx, dora_indicator in enumerate(dora_indicator_list): dora_feature[idx][indicator2dora(dora_indicator)] = 1 return dora_feature def getScoreList1(self, tiles_state_and_action): def trans_score(score): feature = np.zeros((34)) if score > 50000: score = 50000 a = score // (50000.0 / 33) # alpha = a + 1 - (score*33.0/50000) # alpha = round(alpha, 2) # feature[int(a)] = alpha # if a<33: # feature[int(a+1)] = 1-alpha feature[int(a)] = 1 return feature player_seat = tiles_state_and_action["player_id"] scores_list = tiles_state_and_action["scores"] scores_feature = np.zeros((4, 34)) for i in range(4): score = scores_list[player_seat] scores_feature[i] = trans_score(score) player_seat += 1 player_seat %= 4 return scores_feature def getBoard1(self, tiles_state_and_action): def wind(c): if c == 'E': return 27 if c == 'S': return 28 if c == 'W': return 29 if c == 'N': return 30 player_seat = tiles_state_and_action["player_id"] dealer_seat = tiles_state_and_action["dealer"] repeat_dealer = tiles_state_and_action["repeat_dealer"] riichi_bets = tiles_state_and_action["riichi_bets"] player_wind = tiles_state_and_action["player_wind"] prevailing_wind = tiles_state_and_action["prevailing_wind"] dealer_feature = np.zeros((1, 34)) dealer_feature[0][(dealer_seat + 4 - player_seat) % 4] = 1 repeat_dealer_feature = np.zeros((1, 34)) repeat_dealer_feature[0][repeat_dealer] = 1 riichi_bets_feature = np.zeros((1, 34)) riichi_bets_feature[0][riichi_bets] = 1 player_wind_feature = np.zeros((1, 34)) player_wind_feature[0][wind(player_wind)] = 1 prevailing_wind_feature = np.zeros((1, 34)) prevailing_wind_feature[0][wind(prevailing_wind)] = 1 return np.concatenate( (dealer_feature, repeat_dealer_feature, riichi_bets_feature, player_wind_feature, prevailing_wind_feature)) def getLookAheadFeature(self, tiles_state_and_action): # 0 for whether can be discarded # 1 2 3 for shanten # 4 5 6 7 for whether can get 2k 4k 6k 8k points with replacing 3 tiles ---- need review: takes too long! # 8 9 10 for in Shimocha Toimen Kamicha discarded # lookAheadFeature = np.zeros((11, 34)) player_tiles = tiles_state_and_action["player_tiles"] closed_hand_136 = player_tiles.get('closed_hand:', []) open_hands_detail = tiles_state_and_action["open_hands_detail"] melds = self.open_hands_detail_to_melds(open_hands_detail) discarded_tiles_136 = player_tiles.get('discarded_tiles', []) player_seat = tiles_state_and_action["player_id"] enemies_tiles_list = tiles_state_and_action["enemies_tiles"] dora_indicators = tiles_state_and_action["dora"] if (len(enemies_tiles_list) == 3): enemies_tiles_list.insert(player_seat, player_tiles) tiles_could_draw = np.ones(34) * 4 for player in enemies_tiles_list: for tile_set in [ player.get('closed_hand:', []), player.get('open_hand', []), player.get('discarded_tiles', []) ]: for tile in tile_set: tiles_could_draw[tile // 4] -= 1 for dora_tile in dora_indicators: tiles_could_draw[tile // 4] -= 1 def feature_process(i, closed_hand_136, melds, dora_indicators, tiles_could_draw, player_seat, enemies_tiles_list): feature = np.zeros((11, 1)) discard_tiles = [ x for x in [i * 4, i * 4 + 1, i * 4 + 2, i * 4 + 3] if x in closed_hand_136 ] if len(discard_tiles) != 0: discard_tile = discard_tiles[0] feature[0] = 1 closed_left_tiles_34 = TilesConverter.to_34_array( [t for t in closed_hand_136 if t != discard_tile]) shanten = self.calculate_shanten_or_get_from_cache( closed_left_tiles_34) for i in range(3): if shanten <= i: feature[i + 1] = 1 maxscore = 0 #self.canwinbyreplace(closed_left_tiles_34, melds, dora_indicators, #tiles_could_draw, replacelimit = 2) scores = [2000, 4000, 6000, 8000] for i in range(4): if maxscore >= scores[i]: feature[i + 4] = 1 seat = player_seat for i in range(3): seat = (seat + 1) % 4 if discard_tile // 4 in [ t // 4 for t in enemies_tiles_list[seat].get( 'discarded_tiles', []) ]: feature[i + 8] = 1 return feature results = Parallel(n_jobs=8)(delayed( feature_process)(i, closed_hand_136, melds, dora_indicators, tiles_could_draw, player_seat, enemies_tiles_list) for i in range(34)) return np.concatenate(results, axis=1) def getGeneralFeature(self, tiles_state_and_action): return np.concatenate(( #self.getLookAheadFeature(tiles_state_and_action), #(11,34) self.getSelfTiles(tiles_state_and_action), # (12,34) self.getDoraList(tiles_state_and_action), # (5,34) self.getBoard1(tiles_state_and_action), # (5,34) self.getEnemiesTiles(tiles_state_and_action), # (36,34) self.getScoreList1(tiles_state_and_action) # (4,34) )) def ChiFeatureGenerator(self, tiles_state_and_action): """ changed the input from filename to tiles_state_and_action data By Jun Lin """ def _chilist(last_player_discarded_tile, closed_hand_136): res = [] last_player_discarded_tile_34 = last_player_discarded_tile // 4 tile_set = last_player_discarded_tile_34 // 9 if tile_set == 3: return res closed_hand_34_detail = [[] for i in range(34)] for tile in closed_hand_136: closed_hand_34_detail[tile // 4].append(tile) if last_player_discarded_tile_34 > tile_set * 9 + 1: if len(closed_hand_34_detail[last_player_discarded_tile_34 - 1] ) != 0 and len(closed_hand_34_detail[ last_player_discarded_tile_34 - 2]) != 0: res.append([ closed_hand_34_detail[last_player_discarded_tile_34 - 1][0], closed_hand_34_detail[last_player_discarded_tile_34 - 2][0], last_player_discarded_tile ]) if last_player_discarded_tile_34 > tile_set * 9 and last_player_discarded_tile_34 < tile_set * 9 + 8: if len(closed_hand_34_detail[last_player_discarded_tile_34 - 1] ) != 0 and len(closed_hand_34_detail[ last_player_discarded_tile_34 + 1]) != 0: res.append([ closed_hand_34_detail[last_player_discarded_tile_34 - 1][0], closed_hand_34_detail[last_player_discarded_tile_34 + 1][0], last_player_discarded_tile ]) if last_player_discarded_tile_34 < tile_set * 9 + 7: if len(closed_hand_34_detail[last_player_discarded_tile_34 + 1] ) != 0 and len(closed_hand_34_detail[ last_player_discarded_tile_34 + 2]) != 0: res.append([ closed_hand_34_detail[last_player_discarded_tile_34 + 1][0], closed_hand_34_detail[last_player_discarded_tile_34 + 2][0], last_player_discarded_tile ]) return res # res = [] # pairs = [(a, b) for idx, a in enumerate(closed_hand_136) for b in closed_hand_136[idx + 1:]] # for p in pairs: # meld = [last_player_discarded_tile,p[0],p[1]] # if all(tile < 36 for tile in meld) or all(36 <= tile < 72 for tile in meld) or all(36 <= tile < 72 for tile in meld): # meld34 = [tile//4 for tile in meld] # if any(tile+1 in meld34 and tile-1 in meld34 for tile in meld34): # res.append(meld) # return res could_chi = tiles_state_and_action["could_chi"] last_player_discarded_tile = tiles_state_and_action[ "last_player_discarded_tile"] closed_hand_136 = tiles_state_and_action["player_tiles"][ 'closed_hand:'] action = tiles_state_and_action["action"] if could_chi == 1: generalFeature = self.getGeneralFeature(tiles_state_and_action) if action[0] == 'Chi': print(_chilist(last_player_discarded_tile, closed_hand_136)) print(action) for chimeld in _chilist(last_player_discarded_tile, closed_hand_136): last_player_discarded_tile_feature = np.zeros((1, 34)) for chitile in chimeld: last_player_discarded_tile_feature[0][chitile // 4] = 1 x = np.concatenate( (last_player_discarded_tile_feature, generalFeature)) if action[0] == 'Chi' and all( chitile // 4 in [a // 4 for a in action[1]] for chitile in chimeld): y = 1 else: y = 0 # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)), # "labels": to_categorical(y, num_classes=2)} yield x.reshape( (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2) def PonFeatureGenerator(self, tiles_state_and_action): """ changed the input from filename to tiles_state_and_action data By Jun Lin """ last_discarded_tile = tiles_state_and_action[ "last_player_discarded_tile"] closed_hand_136 = tiles_state_and_action["player_tiles"][ 'closed_hand:'] action = tiles_state_and_action["action"] if tiles_state_and_action["could_pon"] == 1: last_discarded_tile_feature = np.zeros((1, 34)) last_discarded_tile_feature[0][last_discarded_tile // 4] = 1 x = np.concatenate( (last_discarded_tile_feature, self.getGeneralFeature(tiles_state_and_action))) if action[0] == 'Pon': y = 1 else: y = 0 # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)), # "labels": to_categorical(y, num_classes=2)} yield x.reshape( (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2) def KanFeatureGenerator(self, tiles_state_and_action): """ changed the input from filename to tiles_state_and_action data By Jun Lin """ def could_ankan(closed_hand_136): count = np.zeros(34) for tile in closed_hand_136: count[tile // 4] += 1 if count[tile // 4] == 4: return True return False def could_kakan(closed_hand_136, open_hand_136): count = np.zeros(34) for tile in open_hand_136: count[tile // 4] += 1 for tile in closed_hand_136: if count[tile // 4] == 3: return True return False last_discarded_tile = tiles_state_and_action[ "last_player_discarded_tile"] closed_hand_136 = tiles_state_and_action["player_tiles"][ 'closed_hand:'] open_hand_136 = tiles_state_and_action["player_tiles"]['open_hand'] action = tiles_state_and_action["action"] if tiles_state_and_action["could_minkan"] == 1: # Minkan kan_type_feature = np.zeros((3, 34)) kan_type_feature[0] = 1 last_discarded_tile_feature = np.zeros((1, 34)) last_discarded_tile_feature[0][last_discarded_tile // 4] = 1 x = np.concatenate( (kan_type_feature, last_discarded_tile_feature, self.getGeneralFeature(tiles_state_and_action))) if action[0] == 'MinKan' and (last_discarded_tile in action[1]): y = 1 else: y = 0 # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)), # "labels": to_categorical(y, num_classes=2)} yield x.reshape( (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2) else: if could_ankan(closed_hand_136): # AnKan kan_type_feature = np.zeros((3, 34)) kan_type_feature[1] = 1 last_discarded_tile_feature = np.zeros((1, 34)) x = np.concatenate( (kan_type_feature, last_discarded_tile_feature, self.getGeneralFeature(tiles_state_and_action))) if action[0] == 'AnKan': y = 1 else: y = 0 # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)), # "labels": to_categorical(y, num_classes=2)} yield x.reshape( (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2) else: if could_kakan(closed_hand_136, open_hand_136): # KaKan kan_type_feature = np.zeros((3, 34)) kan_type_feature[2] = 1 last_discarded_tile_feature = np.zeros((1, 34)) x = np.concatenate( (kan_type_feature, last_discarded_tile_feature, self.getGeneralFeature(tiles_state_and_action))) if action[0] == 'KaKan': y = 1 else: y = 0 # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)), # "labels": to_categorical(y, num_classes=2)} yield x.reshape((x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2) def RiichiFeatureGenerator(self, tiles_state_and_action): action = tiles_state_and_action["action"] if tiles_state_and_action["is_FCH"] == 1: tiles_34 = TilesConverter.to_34_array( tiles_state_and_action["player_tiles"]["closed_hand:"]) # min_shanten = self.shanten_calculator.calculate_shanten(tiles_34) min_shanten = self.calculate_shanten_or_get_from_cache(tiles_34) if min_shanten == 0: x = self.getGeneralFeature(tiles_state_and_action) if action[0] == 'REACH': y = 1 else: y = 0 # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)), # "labels": to_categorical(y, num_classes=2)} yield x.reshape( (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2) def DiscardFeatureGenerator(self, tiles_state_and_action): x = self.getGeneralFeature(tiles_state_and_action) y = tiles_state_and_action["discarded_tile"] // 4 yield x.reshape( (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=34)
def score(boxes, tsumo, y_divider, round_wind=None, player_wind=None): """ +---------------+ | _ _ _ _ _ | | |_|_|_|_| |_| | classfied to `open` | | |---------------| <- y_divider | _ _ _ _ _ _ | | |_|_|_|_|_|_| | classfied to `closed` | | +---------------+ boxes: array of box which elements are: tile id, xmin, ymin, xmax and ymax. e.g. [[0, 100, 200, 150, 250], [2, 200, 300, 350, 350], [...]] the right most of the tile is assumed as win tile, and ron or tsumo is specified by tsumo. tile id is: man1...man9: 0...8 pin1...pin9: 9...17 sou1...sou9: 18...26 ton, nan, sha, pei: 27, 28, 29, 30 haku, hatsu, chun: 31, 32, 33 y_divider: tiles are divided into two types; open or closed. if the position is upper than y_divider, the tile is classfied as open. """ open_boxes, win_box, closed_boxes = classify_boxes(boxes, y_divider) if open_boxes is None: return None melds = make_melds(open_boxes) if melds is None: return None # 4-tile-set of kan should be trimed to 3-tile-set. open_kan = [ x.tiles_34[0] for x in melds if x.type == Meld.KAN and x.opened ] open_tiles = boxes_to_tiles(open_boxes, open_kan) if open_tiles is None: return None win_tile = boxes_to_tiles([win_box]) if win_tile is None: return None closed_kan_melds, closed_boxes_replaced = make_closed_kan_melds( closed_boxes) # 4-tile-set of kan should be trimed to 3-tile-set. closed_kan = [x.tiles_34[0] for x in closed_kan_melds] closed_tiles = boxes_to_tiles(closed_boxes_replaced, closed_kan) if closed_tiles is None: return None hand = closed_tiles + open_tiles + win_tile print("win:\n", tc.to_one_line_string(win_tile)) print("hand:\n", tc.to_one_line_string(hand)) print("melds:\n", melds + closed_kan_melds) for meld in melds: print(tc.to_one_line_string(meld.tiles)) options = OptionalRules(has_open_tanyao=True, kazoe_limit=HandConfig.KAZOE_NO_LIMIT) config = HandConfig( is_tsumo=tsumo, player_wind=player_wind, round_wind=round_wind, options=options, ) print("tsumo:", tsumo, " player:", player_wind, " round:", round_wind) result = HandCalculator().estimate_hand_value(hand, win_tile[0], melds=melds + closed_kan_melds, config=config) print_hand_result(result) return result
class MahjongAI: version = "0.5.1" agari = None shanten_calculator = None defence = None riichi = None hand_divider = None finished_hand = None shanten = 7 ukeire = 0 ukeire_second = 0 waiting = None current_strategy = None last_discard_option = None hand_cache_shanten = {} hand_cache_estimation = {} def __init__(self, player): self.player = player self.table = player.table self.kan = Kan(player) self.agari = Agari() self.shanten_calculator = Shanten() self.defence = TileDangerHandler(player) self.riichi = Riichi(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.hand_builder = HandBuilder(player, self) self.placement = player.config.PLACEMENT_HANDLER_CLASS(player) self.suji = Suji(player) self.kabe = Kabe(player) self.erase_state() def erase_state(self): self.shanten = 7 self.ukeire = 0 self.ukeire_second = 0 self.waiting = None self.current_strategy = None self.last_discard_option = None self.hand_cache_shanten = {} self.hand_cache_estimation = {} # to erase hand cache self.finished_hand = HandCalculator() def init_hand(self): self.player.logger.debug( log.INIT_HAND, context=[ f"Round wind: {DISPLAY_WINDS[self.table.round_wind_tile]}", f"Player wind: {DISPLAY_WINDS[self.player.player_wind]}", f"Hand: {self.player.format_hand_for_print()}", ], ) self.shanten, _ = self.hand_builder.calculate_shanten_and_decide_hand_structure( TilesConverter.to_34_array(self.player.tiles)) def draw_tile(self, tile_136): if not self.player.in_riichi: self.determine_strategy(self.player.tiles) def discard_tile(self, discard_tile): # we called meld and we had discard tile that we wanted to discard if discard_tile is not None: if not self.last_discard_option: return discard_tile, False return self.hand_builder.process_discard_option( self.last_discard_option) return self.hand_builder.discard_tile() def try_to_call_meld(self, tile_136, is_kamicha_discard, meld_type=None): tiles_136_previous = self.player.tiles[:] closed_hand_136_previous = self.player.closed_hand[:] tiles_136 = tiles_136_previous + [tile_136] self.determine_strategy(tiles_136, meld_tile=tile_136) if not self.current_strategy: self.player.logger.debug( log.MELD_DEBUG, "We don't have active strategy. Abort melding.") return None, None closed_hand_34_previous = TilesConverter.to_34_array( closed_hand_136_previous) previous_shanten, _ = self.hand_builder.calculate_shanten_and_decide_hand_structure( closed_hand_34_previous) 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 self.player.logger.debug( log.MELD_CALL, "We decided to open hand", context=[ f"Hand: {self.player.format_hand_for_print(tile_136)}", f"Meld: {meld.serialize()}", f"Discard after meld: {discard_option.serialize()}", ], ) return meld, discard_option def determine_strategy(self, tiles_136, meld_tile=None): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False old_strategy = self.current_strategy self.current_strategy = None # order is important, we add strategies with the highest priority first strategies = [] if self.player.table.has_open_tanyao: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) strategies.append(YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player)) strategies.append(HonitsuStrategy(BaseStrategy.HONITSU, self.player)) strategies.append(ChinitsuStrategy(BaseStrategy.CHINITSU, self.player)) strategies.append( FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player)) strategies.append( CommonOpenTempaiStrategy(BaseStrategy.COMMON_OPEN_TEMPAI, self.player)) for strategy in strategies: if strategy.should_activate_strategy(tiles_136, meld_tile=meld_tile): self.current_strategy = strategy break if self.current_strategy and ( not old_strategy or self.current_strategy.type != old_strategy.type): self.player.logger.debug( log.STRATEGY_ACTIVATE, context=self.current_strategy, ) if not self.current_strategy and old_strategy: self.player.logger.debug(log.STRATEGY_DROP, context=old_strategy) return self.current_strategy and True or False def estimate_hand_value_or_get_from_cache(self, win_tile_34, tiles=None, call_riichi=False, is_tsumo=False, is_rinshan=False, is_chankan=False): win_tile_136 = win_tile_34 * 4 # we don't need to think, that our waiting is aka dora if win_tile_136 in AKA_DORA_LIST: win_tile_136 += 1 if not tiles: tiles = self.player.tiles[:] else: tiles = tiles[:] tiles += [win_tile_136] config = HandConfig( is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind_tile, is_tsumo=is_tsumo, is_rinshan=is_rinshan, is_chankan=is_chankan, options=OptionalRules( has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao, has_double_yakuman=False, ), tsumi_number=self.player.table.count_of_honba_sticks, kyoutaku_number=self.player.table.count_of_riichi_sticks, ) return self._estimate_hand_value_or_get_from_cache( win_tile_136, tiles, call_riichi, is_tsumo, 0, config, is_rinshan, is_chankan) def calculate_exact_hand_value_or_get_from_cache( self, win_tile_136, tiles=None, call_riichi=False, is_tsumo=False, is_chankan=False, is_haitei=False, is_ippatsu=False, ): if not tiles: tiles = self.player.tiles[:] else: tiles = tiles[:] tiles += [win_tile_136] additional_han = 0 if is_chankan: additional_han += 1 if is_haitei: additional_han += 1 if is_ippatsu: additional_han += 1 config = HandConfig( is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind_tile, is_tsumo=is_tsumo, options=OptionalRules( has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao, has_double_yakuman=False, ), is_chankan=is_chankan, is_ippatsu=is_ippatsu, is_haitei=is_tsumo and is_haitei or False, is_houtei=(not is_tsumo) and is_haitei or False, tsumi_number=self.player.table.count_of_honba_sticks, kyoutaku_number=self.player.table.count_of_riichi_sticks, ) return self._estimate_hand_value_or_get_from_cache( win_tile_136, tiles, call_riichi, is_tsumo, additional_han, config) def _estimate_hand_value_or_get_from_cache(self, win_tile_136, tiles, call_riichi, is_tsumo, additional_han, config, is_rinshan=False, is_chankan=False): cache_key = build_estimate_hand_value_cache_key( tiles, call_riichi, is_tsumo, self.player.melds, self.player.table.dora_indicators, self.player.table.count_of_riichi_sticks, self.player.table.count_of_honba_sticks, additional_han, is_rinshan, is_chankan, ) if self.hand_cache_estimation.get(cache_key): return self.hand_cache_estimation.get(cache_key) result = self.finished_hand.estimate_hand_value( tiles, win_tile_136, self.player.melds, self.player.table.dora_indicators, config, use_hand_divider_cache=True, ) self.hand_cache_estimation[cache_key] = result return result def estimate_weighted_mean_hand_value(self, discard_option): weighted_hand_cost = 0 number_of_tiles = 0 for waiting in discard_option.waiting: tiles = self.player.tiles[:] tiles.remove(discard_option.tile_to_discard_136) hand_cost = self.estimate_hand_value_or_get_from_cache( waiting, tiles=tiles, call_riichi=discard_option.with_riichi, is_tsumo=True) if not hand_cost.cost: continue weighted_hand_cost += (hand_cost.cost["main"] + 2 * hand_cost.cost["additional"] ) * discard_option.wait_to_ukeire[waiting] number_of_tiles += discard_option.wait_to_ukeire[waiting] cost = number_of_tiles and int( weighted_hand_cost / number_of_tiles) or 0 # we are karaten, or we don't have yaku # in that case let's add possible tempai cost if cost == 0 and self.player.round_step > 12: cost = 1000 if self.player.round_step > 15 and cost < 2500: cost = 2500 return cost def should_call_kyuushu_kyuuhai(self) -> bool: """ Kyuushu kyuuhai 「九種九牌」 (9 kinds of honor or terminal tiles) """ # TODO aim for kokushi return True def should_call_win(self, tile, is_tsumo, enemy_seat=None, is_chankan=False): # don't skip win in riichi if self.player.in_riichi: return True # currently we don't support win skipping for tsumo if is_tsumo: return True # fast path - check it first to not calculate hand cost cost_needed = self.placement.get_minimal_cost_needed() if cost_needed == 0: return True # 1 and not 0 because we call check for win this before updating remaining tiles is_hotei = self.player.table.count_of_remaining_tiles == 1 hand_response = self.calculate_exact_hand_value_or_get_from_cache( tile, tiles=self.player.tiles, call_riichi=self.player.in_riichi, is_tsumo=is_tsumo, is_chankan=is_chankan, is_haitei=is_hotei, ) assert hand_response is not None assert not hand_response.error, hand_response.error cost = hand_response.cost return self.placement.should_call_win(cost, is_tsumo, enemy_seat) def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ pass def calculate_shanten_or_get_from_cache(self, closed_hand_34: List[int], use_chiitoitsu: bool): """ Sometimes we are calculating shanten for the same hand multiple times to save some resources let's cache previous calculations """ key = build_shanten_cache_key(closed_hand_34, use_chiitoitsu) if key in self.hand_cache_shanten: return self.hand_cache_shanten[key] if use_chiitoitsu and not self.player.is_open_hand: result = self.shanten_calculator.calculate_shanten_for_chiitoitsu_hand( closed_hand_34) else: result = self.shanten_calculator.calculate_shanten_for_regular_hand( closed_hand_34) self.hand_cache_shanten[key] = result return result @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:]
class ImplementationAI(InterfaceAI): version = '0.0.1' agari = None shanten = None defence = None hand_divider = None finished_hand = None last_discard_option = None previous_shanten = 7 in_defence = False waiting = None current_strategy = None def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() self.shanten = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.previous_shanten = 7 self.current_strategy = None self.waiting = [] self.in_defence = False self.last_discard_option = None def init_hand(self): """ Let's decide what we will do with our hand (like open for tanyao and etc.) """ self.determine_strategy() def erase_state(self): self.current_strategy = None self.in_defence = False self.last_discard_option = None def draw_tile(self, tile): """ :param tile: 136 tile format :return: """ self.determine_strategy() def discard_tile(self, discard_tile): # we called meld and we had discard tile that we wanted to discard if discard_tile is not None: if not self.last_discard_option: return discard_tile return self.process_discard_option(self.last_discard_option, self.player.closed_hand, True) results, shanten = self.calculate_outs(self.player.tiles, self.player.closed_hand, self.player.open_hand_34_tiles) selected_tile = self.process_discard_options_and_select_tile_to_discard( results, shanten) # bot think that there is a threat on the table # and better to fold # if we can't find safe tiles, let's continue to build our hand if self.defence.should_go_to_defence_mode(selected_tile): if not self.in_defence: logger.info('We decided to fold against other players') self.in_defence = True defence_tile = self.defence.try_to_find_safe_tile_to_discard( results) if defence_tile: return self.process_discard_option(defence_tile, self.player.closed_hand) else: self.in_defence = False return self.process_discard_option(selected_tile, self.player.closed_hand) def process_discard_options_and_select_tile_to_discard( self, results, shanten, had_was_open=False): tiles_34 = TilesConverter.to_34_array(self.player.tiles) # we had to update tiles value there # because it is related with shanten number for result in results: result.tiles_count = self.count_tiles(result.waiting, tiles_34) result.calculate_value(shanten) # current strategy can affect on our discard options # so, don't use strategy specific choices for calling riichi if self.current_strategy: results = self.current_strategy.determine_what_to_discard( self.player.closed_hand, results, shanten, False, None, had_was_open) return self.chose_tile_to_discard(results) 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 count_tiles(self, waiting, tiles_34): n = 0 for item in waiting: n += 4 - self.player.total_tiles(item, tiles_34) return n def try_to_call_meld(self, tile, is_kamicha_discard): if not self.current_strategy: return None, None if len(self.player.discards) <= 5 and tile // 4 <= 27: return None, None meld, discard_option = self.current_strategy.try_to_call_meld( tile, is_kamicha_discard) tile_to_discard = None if discard_option: self.last_discard_option = discard_option tile_to_discard = discard_option.tile_to_discard return meld, tile_to_discard def determine_strategy(self): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False return False old_strategy = self.current_strategy self.current_strategy = None # order is important, the first appropriate strtegy will be used strategies = [] if self.player.table.has_open_tanyao: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) strategies.append(YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player)) strategies.append(HonitsuStrategy(BaseStrategy.HONITSU, self.player)) for strategy in strategies: if strategy.should_activate_strategy(): self.current_strategy = strategy if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: message = '{} switched to {} strategy'.format( self.player.name, self.current_strategy) if old_strategy: message += ' from {}'.format(old_strategy) logger.debug(message) logger.debug('With hand: {}'.format( TilesConverter.to_one_line_string(self.player.tiles))) if not self.current_strategy and old_strategy: logger.debug('{} gave up on {}'.format(self.player.name, old_strategy)) return self.current_strategy and True or False def chose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: """ Try to find best tile to discard, based on different valuations """ def sorting(x): # - is important for x.tiles_count # in that case we will discard tile that will give for us more tiles # to complete a hand return x.shanten, -x.tiles_count, x.valuation had_to_be_discarded_tiles = [ x for x in results if x.had_to_be_discarded ] if had_to_be_discarded_tiles: had_to_be_discarded_tiles = sorted(had_to_be_discarded_tiles, key=sorting) selected_tile = had_to_be_discarded_tiles[0] else: results = sorted(results, key=sorting) # remove needed tiles from discard options results = [x for x in results if not x.had_to_be_saved] # let's chose most valuable tile first temp_tile = results[0] # and let's find all tiles with same shanten results_with_same_shanten = [ x for x in results if x.shanten == temp_tile.shanten ] possible_options = [temp_tile] for discard_option in results_with_same_shanten: # there is no sense to check already chosen tile if discard_option.tile_to_discard == temp_tile.tile_to_discard: continue # we don't need to select tiles almost dead waits if discard_option.tiles_count <= 2: continue # let's check all other tiles with same shanten # maybe we can find tiles that have almost same tiles count number if temp_tile.tiles_count - 2 < discard_option.tiles_count < temp_tile.tiles_count + 2: possible_options.append(discard_option) # let's sort got tiles by value and let's chose less valuable tile to discard possible_options = sorted(possible_options, key=lambda x: x.valuation) selected_tile = possible_options[0] return selected_tile def process_discard_option(self, discard_option, closed_hand, force_discard=False): self.waiting = discard_option.waiting self.player.ai.previous_shanten = discard_option.shanten self.player.in_tempai = self.player.ai.previous_shanten == 0 # when we called meld we don't need "smart" discard if force_discard: return discard_option.find_tile_in_hand(closed_hand) last_draw_34 = self.player.last_draw and self.player.last_draw // 4 or None if self.player.last_draw not in AKA_DORA_LIST and last_draw_34 == discard_option.tile_to_discard: return self.player.last_draw else: return discard_option.find_tile_in_hand(closed_hand) def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): """ :param win_tile: 34 tile format :param tiles: :param call_riichi: :return: """ win_tile *= 4 # we don't need to think, that our waiting is aka dora if win_tile in AKA_DORA_LIST: win_tile += 1 if not tiles: tiles = self.player.tiles tiles += [win_tile] config = HandConfig(is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind, has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao) result = self.finished_hand.estimate_hand_value( tiles, win_tile, self.player.melds, self.player.table.dora_indicators, config) return result def should_call_riichi(self): print(self.player.discards) # empty waiting can be found in some cases if not self.waiting: return False if self.in_defence: return False #If we tenpai fast enough if len(self.player.discards) <= 8: return True if len(self.player.discards) >= 14: return False # we have a good wait, let's riichi if len(self.waiting) > 1: return True waiting = self.waiting[0] tiles = self.player.closed_hand + [waiting * 4] closed_melds = [x for x in self.player.melds if not x.opened] for meld in closed_melds: tiles.extend(meld.tiles[:3]) tiles_34 = TilesConverter.to_34_array(tiles) results = self.hand_divider.divide_hand(tiles_34) result = results[0] count_of_pairs = len([x for x in result if is_pair(x)]) # with chitoitsu we can call a riichi with pair wait if count_of_pairs == 7: return True for hand_set in result: # better to not call a riichi for a pair wait # it can be easily improved if is_pair(hand_set) and waiting in hand_set: return False return True def should_call_kan(self, tile, open_kan): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :return: kan type """ # 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) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # let's check can we upgrade opened pon to the kan if pon_melds: for meld in pon_melds: # tile is equal to our already opened pon, # so let's call chankan! 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 # we have 3 tiles in our hand, # so we can try to call closed meld 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) # called kan will not ruin our hand if new_shanten <= previous_shanten: return Meld.KAN return None def should_call_win(self, tile, enemy_seat): return True def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ if self.defence.should_go_to_defence_mode(): self.in_defence = True @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:]
class MonteCarlo(object): """A simulator to run Monte Carlo simulation on Mahjong game """ decoder = TenhouDecoder() agari = Agari() finished_hand = FinishedHand() hand_calculator = HandCalculator() verbose = False def __init__(self): previous_ai = False table = MCVisibleTable(previous_ai) self.table = table self.initialize() # We load the classifiers and regressors here self.clf_one_player = [] for n in range(34): clf = pickle.load( open( abs_data_path + "/train_model/trained_models/one_player_{}.sav".format(n), "rb")) self.clf_one_player.append(clf) def initialize(self): # Mahjong table is ready # previous_ai = False # table = MCVisibleTable(previous_ai) # self.table = table self.table.erase_state() self.table.turn_number = [0, 0, 0, 0] # Prepare a new deck of mahjong tiles tiles = list(range(136)) # shuffle the tiles random.shuffle(tiles) # seperate dead wall and dora indicator dead_wall = tiles[-14:] dora_indicator = dead_wall[4] # random.choice(dead_wall) self.table.dead_wall = dead_wall # generate init hands for 4 players tiles = tiles[0:-14] hands = [] for n in range(4): init_tiles = tiles[0:13] hands.append(init_tiles) tiles = tiles[13:] self.table.remaining_tiles = tiles # formulate the init message dice1 = random.choice([1, 2, 3, 4, 5, 6]) dice2 = random.choice([1, 2, 3, 4, 5, 6]) dealer = (dice1 - 1 + dice2 - 1) % 4 message = "<INIT " # TODO: You may need round number, number of combo sticks, and number of # riichi sticks other than 0? message += 'seed="{},{},{},{},{},{}" '.format(0, 0, 0, dice1 - 1, dice2 - 1, dora_indicator) message += 'ten="250,250,250,250" ' message += 'oya="{}" '.format(dealer) message += 'hai0="{}" '.format(",".join(str(t) for t in hands[0])) message += 'hai1="{}" '.format(",".join(str(t) for t in hands[1])) message += 'hai2="{}" '.format(",".join(str(t) for t in hands[2])) message += 'hai3="{}"/>'.format(",".join(str(t) for t in hands[3])) # once message is ready, we decoder the info and initialize the table values = self.decoder.parse_initial_values(message) self.table.init_round( values['round_number'], values['count_of_honba_sticks'], values['count_of_riichi_sticks'], values['dora_indicator'], values['dealer'], values['scores'], ) # TODO: this part is unnecessary, since we have already the hands list hands = [ [ int(x) for x in self.decoder.get_attribute_content( message, 'hai0').split(',') ], [ int(x) for x in self.decoder.get_attribute_content( message, 'hai1').split(',') ], [ int(x) for x in self.decoder.get_attribute_content( message, 'hai2').split(',') ], [ int(x) for x in self.decoder.get_attribute_content( message, 'hai3').split(',') ], ] # Initialize all players on the table # TODO: ok, we always assume we are sitting at seat 0 self.player_position = 0 self.table.players[0].init_hand(hands[self.player_position]) self.table.players[1].init_hand(hands[(self.player_position + 1) % 4]) self.table.players[2].init_hand(hands[(self.player_position + 2) % 4]) self.table.players[3].init_hand(hands[(self.player_position + 3) % 4]) # main_player = self.table.player # print(self.table.__str__()) # print('Players: {}'.format(self.table.get_players_sorted_by_scores())) # print('Dealer: {}'.format(self.table.get_player(values['dealer']))) # print('Round wind: {}'.format(DISPLAY_WINDS[self.table.round_wind])) # print('Player wind: {}'.format(DISPLAY_WINDS[main_player.player_wind])) # TODO: this part may not be necessary. Basically we need to erase the # melds information since it is a new game. # If we are using opponent_model, we need to reset the melds # when a new game initiated. For other model, this function # may not exist. [Joseph] try: self.table.player.ai.reset_melds() except: pass def _check_win(self, player_hand, melds): """ check the hand to see whether it's a win hand or not :param player_hand: list of int (0-33), the player's hand tiles in 34 format :return: True if win, else False. """ return self.agari.is_agari(player_hand, melds) def check_win(self, p): """check win hand :param p: int (0-3), player index :return: True if win, False if not. """ player_hand = self.table.players[p].tiles player_hand_34 = TilesConverter.to_34_array(player_hand) melds = [meld.tiles for meld in self.table.players[p].melds ] # self.table.players[p].melds is [list of Meld()] melds_34 = list(map(lambda lst: [l // 4 for l in lst], melds)) return self._check_win(player_hand_34, melds_34) def _check_waiting(self, player_hand): """Check whether a player is waiting or not based on his hand :param player_hand: list of elements in [0,133], the hand tiles of player :return: True for waiting, False for not waiting """ # If we need one more tile to complete our hand, and this specific tile # we want is known to be within the wall tiles, then the hand is waiting. current_hand = TilesConverter.to_34_array(player_hand) #winning_tiles = [] for n in range(34): table_revealed_tiles_34 = self.table.revealed_tiles if table_revealed_tiles_34[n] < 4: completed_hand = current_hand[:] completed_hand[n] += 1 can_be_waiting = self.agari.is_agari(completed_hand) if can_be_waiting: return True #winning_tiles.append(n) # n is the winning tile we want # If there exists winnint tiles, the player is waitinng #if len(winning_tiles)>0: # return True return False def check_waiting(self, p): """check whether the player `p` is waiting or not :param p: int (0-3), player index :return: True for waiting, False for not waiting """ player_hand = self.table.players[p].tiles return self._check_waiting(player_hand) def discard_tile(self, p): """choose a tile to discard based on hand tiles of player `p` :param p: int(0-3), player index :return: int(0-135), tile to discard """ tiles = self.table.players[p].tiles closed_hand = self.table.players[p].closed_hand open_sets_34 = self.table.players[p].open_hand_34_tiles table_revealed_tiles_34 = self.table.revealed_tiles discarded_tile = self.table.players[p].ai.discard_tile( tiles, closed_hand, open_sets_34, table_revealed_tiles_34) return discarded_tile def call_meld(self, type, who, from_who, opened, tiles, called_tile): meld = Meld() meld.type = type meld.who = who meld.from_who = from_who meld.opened = opened meld.tiles = tiles meld.called_tile = called_tile return meld def sim_WPET(self): """Simulation of a riichi mahjong game, and record the bw and r parameters """ # We don't need to fix the seed now. #seed = random.randint(1,100000) #seed = 73823 #random.seed(seed) bw_riichi = 0 bw_stealing = 0 r = 0 # start a new game self.initialize() # rinshan rinshan = self.table.dead_wall[0:4] # start from dealer p = self.table.dealer_seat #logger.info("seed: %d"%seed) logger.info("dealer seat: %d" % p) # Fixed: self.table.count_of_remaining_tiles>0 may not be accurate here while self.table.count_of_remaining_tiles > 0: #len(self.table.remaining_tiles)>0: # our perspective of view is from the program's turn if p == 0: r += 1 if self.check_waiting(0): # TODO: Why don't we use in_riichi here? if self.table.players[0].is_open_hand: bw_stealing = 1 else: bw_riichi = 1 break logger.debug("[begin]:%s,%s" % (self.table.count_of_remaining_tiles, len(self.table.remaining_tiles))) # p draw a tile drawn_tile = self.table.remaining_tiles[0] self.table.players[p].draw_tile(drawn_tile) # check p win if self.check_win(p): logger.info("player {} wins (tsumo)!".format(p)) break # TODO: we only apply discard_tile strategy to our program, for the # opponent players, we assume they simply discard what they draw # if not win, we do the followings if True: #p==0: if not self.table.players[p].in_riichi: # choose a discard tile for playe `p` discarded_tile = self.discard_tile(p) # see if we can call riichi if self.table.players[p].can_call_riichi(): logger.debug("player {} call riichi.".format(p)) self.table.players[p].in_riichi = True else: # if riichi, we have to discard whatever we draw discarded_tile = drawn_tile else: discarded_tile = drawn_tile # remove the tile from player's hand discarded_tile_tmp = discarded_tile drawn_tile_tmp = drawn_tile is_tsumogiri = (discarded_tile == drawn_tile) self.table.players[p].tiles.remove(discarded_tile) logger.debug("player {} discards {}".format(p, discarded_tile)) logger.debug("\tclosed hand: %s" % self.table.players[0].closed_hand) logger.debug("\topen hand: %s" % self.table.players[0].open_hand_34_tiles) logger.debug("\tmeld tiles: %s" % self.table.players[0].meld_tiles) # now we check call meld # TODO: Ok, here we only allow our program to call meld. But maybe # we should allow the opponents to call meld too? if p != 0: previous_drawn_tile = drawn_tile tile = discarded_tile is_kamicha_discard = (p == 3) meld, discard_option = self.table.players[0].try_to_call_meld( tile, is_kamicha_discard) kan_type = self.table.players[0].can_call_kan(tile, True) if kan_type: # kan or chankan tiles = [(tile // 4) * 4, (tile // 4) * 4 + 1, (tile // 4) * 4 + 2, (tile // 4) * 4 + 3] meld = self.call_meld(kan_type, 0, p, True, tiles, tile) logger.debug("player 0 call kan from %d: %s" % (p, meld)) player_seat = meld.who self.table.add_called_meld(player_seat, meld) # we had to delete called tile from hand # to have correct tiles count in the hand # TODO[joseph]: I still don't know why we need this # if meld type is: chi, pon, or nuki # maybe no use here, b/c the meld type is always Meld.KAN here if meld.type != Meld.KAN and meld.type != Meld.CHANKAN: self.table.players[player_seat].draw_tile( meld.called_tile) # draw a tile from dead wall if len(rinshan) > 0: drawn_tile = rinshan.pop(0) self.table.players[0].draw_tile(drawn_tile) # check p win if self.check_win(0): logger.info( "player {} wins (rinshan tsumo)!".format(0)) break # if not win, we do the followings if not self.table.players[0].in_riichi: # choose a discard tile for playe `p` discarded_tile = self.discard_tile(0) # see if we can call riichi if self.table.players[0].can_call_riichi(): logger.info("player {} call riichi.".format(0)) self.table.players[0].in_riichi = True else: # if riichi, we have to discard whatever we draw discarded_tile = drawn_tile # remove the tile from player's hand self.table.players[0].tiles.remove(discarded_tile) logger.debug("player {} discards {} after kan".format( 0, discarded_tile)) logger.debug("\tclosed hand: %s" % self.table.players[0].closed_hand) logger.debug("\topen hand: %s" % self.table.players[0].open_hand_34_tiles) logger.debug("\tmeld tiles: %s" % self.table.players[0].meld_tiles) # we had to add it to discards, to calculate remaining tiles correctly # drawn tile is not the one drawn from rinshan, but # the one previously discarded by player `p` self.table.add_discarded_tile(0, discarded_tile, True, previous_drawn_tile) # after program discarding a card, next player is 1 p = 1 continue else: # ryuukyoku logger.debug("Rinshan empty. Ryuukyoku!") break elif meld: # pon, chi logger.debug("player 0 %s from %d: %s" % (meld.type, p, meld)) player_seat = 0 # DEBUG: we change the add_called_meld method, delete the # part that changes self.table.count_of_remaining_tiles self.table.add_called_meld(player_seat, meld) # Equivalently, program draws the tile discarded by opponent self.table.players[0].draw_tile(tile) # check p win if self.check_win(0): logger.info("player {} wins (by {})!".format( 0, meld.type)) break # if not win, we do the followings if not self.table.players[0].in_riichi: # choose a discard tile for playe `p` discarded_tile = self.discard_tile(0) # see if we can call riichi if self.table.players[0].can_call_riichi(): logger.debug("player {} call riichi.".format(0)) self.table.players[0].in_riichi = True else: # if riichi, we can not call meld raise ("Riichi player can not call meld!") # remove the tile from player's hand self.table.players[0].tiles.remove(discarded_tile) # discarded tile added to table self.table.add_discarded_tile(0, discarded_tile, True, previous_drawn_tile) logger.debug("player {} discards {} after {}".format( 0, discarded_tile, meld.type)) logger.debug("\tclosed hand: %s" % self.table.players[0].closed_hand) logger.debug("\topen hand: %s" % self.table.players[0].open_hand_34_tiles) logger.debug("\tmeld tiles: %s" % self.table.players[0].meld_tiles) # after program discarding a card, next player is 1 p = 1 continue # we had to add it to discards, to calculate remaining tiles correctly self.table.add_discarded_tile(p, discarded_tile_tmp, is_tsumogiri, drawn_tile_tmp) # next player p = (p + 1) % 4 logger.debug("[after]:%s,%s" % (self.table.count_of_remaining_tiles, len(self.table.remaining_tiles))) # output results logger.debug('\n') for p in range(4): logger.info("\tPlayer %d: %s (%s)" % (p, TilesConverter.to_one_line_string( self.table.players[p].tiles), TilesConverter.to_one_line_string( self.table.players[p].closed_hand))) return bw_riichi, bw_stealing, r def get_WPET(self, Nsim): """Waiting probability at Each Turn """ R = [0] * 19 BW_riichi = [0] * 19 BW_stealing = [0] * 19 n = 0 while n < Nsim: bw_riichi, bw_stealing, r = self.sim_WPET() print("%d. bw_riichi=%s, bw_stealing=%s, r=%s" % (n + 1, bw_riichi, bw_stealing, r)) for m in range(r + 1): # from 0 to r R[m] += 1 BW_riichi[r] += bw_riichi BW_stealing[r] += bw_stealing n += 1 Wpet_riichi = [ BW_riichi[n] * 1.0 / R[n] for n in range(len(BW_riichi)) ] Wpet_stealing = [ BW_stealing[n] * 1.0 / R[n] for n in range(len(BW_stealing)) ] return BW_riichi, BW_stealing, R, Wpet_riichi, Wpet_stealing def sim_game(self): """Simulation of a riichi mahjong game, and record the bw and r parameters """ # We don't need to fix the seed now. #seed = random.randint(1,100000) #seed = 73823 #random.seed(seed) # start a new game self.initialize() # rinshan rinshan = self.table.dead_wall[0:4] # start from dealer p = self.table.dealer_seat #logger.info("seed: %d"%seed) logger.info("dealer seat: %d" % p) # Fixed: self.table.count_of_remaining_tiles>0 may not be accurate here while self.table.count_of_remaining_tiles > 0: #len(self.table.remaining_tiles)>0: logger.debug("[begin]:%s,%s" % (self.table.count_of_remaining_tiles, len(self.table.remaining_tiles))) # update turn number self.table.turn_number[p] += 1 # p draw a tile drawn_tile = self.table.remaining_tiles[0] self.table.players[p].draw_tile(drawn_tile) # check p win if self.check_win(p): logger.info("player {} wins (tsumo)!".format(p)) tiles = self.table.players[p].tiles win_tile = drawn_tile (is_tsumo, is_riichi, is_dealer, open_sets, dora_indicators, player_wind, round_wind) = self.check_status(p) ## TODO: Wait to be finished! result = self.finished_hand.estimate_hand_value( tiles, win_tile, is_tsumo=is_tsumo, is_riichi=is_riichi, is_dealer=is_dealer, is_ippatsu=False, is_rinshan=False, is_chankan=False, is_haitei=False, is_houtei=False, is_daburu_riichi=False, is_nagashi_mangan=False, is_tenhou=False, is_renhou=False, is_chiihou=False, open_sets=open_sets, dora_indicators=dora_indicators, called_kan_indices=None, player_wind=player_wind, round_wind=round_wind) logger.info(result) melds = self.table.players[p].melds result = self.hand_calculator.estimate_hand_value( tiles, win_tile, melds=melds, dora_indicators=dora_indicators, config=HandConfig(is_tsumo=is_tsumo, is_riichi=is_riichi, player_wind=player_wind, round_wind=round_wind)) logger.info(result) break # TODO: we only apply discard_tile strategy to our program, for the # opponent players, we assume they simply discard what they draw # if not win, we do the followings if True: #p==0: if not self.table.players[p].in_riichi: # choose a discard tile for playe `p` discarded_tile = self.discard_tile(p) # see if we can call riichi if self.table.players[p].can_call_riichi(): logger.debug("player {} call riichi.".format(p)) self.table.players[p].in_riichi = True else: # if riichi, we have to discard whatever we draw discarded_tile = drawn_tile else: discarded_tile = drawn_tile # remove the tile from player's hand discarded_tile_tmp = discarded_tile drawn_tile_tmp = drawn_tile is_tsumogiri = (discarded_tile == drawn_tile) self.table.players[p].tiles.remove(discarded_tile) logger.debug("player {} discards {}".format(p, discarded_tile)) logger.debug("\tclosed hand: %s" % self.table.players[0].closed_hand) logger.debug("\topen hand: %s" % self.table.players[0].open_hand_34_tiles) logger.debug("\tmeld tiles: %s" % self.table.players[0].meld_tiles) # now we check call meld # TODO: Ok, here we only allow our program to call meld. But maybe # we should allow the opponents to call meld too? if p != 0: previous_drawn_tile = drawn_tile tile = discarded_tile is_kamicha_discard = (p == 3) meld, discard_option = self.table.players[0].try_to_call_meld( tile, is_kamicha_discard) kan_type = self.table.players[0].can_call_kan(tile, True) if kan_type: # kan or chankan tiles = [(tile // 4) * 4, (tile // 4) * 4 + 1, (tile // 4) * 4 + 2, (tile // 4) * 4 + 3] meld = self.call_meld(kan_type, 0, p, True, tiles, tile) logger.debug("player 0 call kan from %d: %s" % (p, meld)) player_seat = meld.who self.table.add_called_meld(player_seat, meld) # we had to delete called tile from hand # to have correct tiles count in the hand # TODO[joseph]: I still don't know why we need this # if meld type is: chi, pon, or nuki # maybe no use here, b/c the meld type is always Meld.KAN here if meld.type != Meld.KAN and meld.type != Meld.CHANKAN: self.table.players[player_seat].draw_tile( meld.called_tile) # draw a tile from dead wall if len(rinshan) > 0: drawn_tile = rinshan.pop(0) self.table.players[0].draw_tile(drawn_tile) # check p win if self.check_win(0): logger.info( "player {} wins (rinshan tsumo)!".format(0)) tiles = self.table.players[p].tiles win_tile = drawn_tile (is_tsumo, is_riichi, is_dealer, open_sets, dora_indicators, player_wind, round_wind) = self.check_status(p) is_rinshan = True ## TODO: Wait to be finished! result = self.finished_hand.estimate_hand_value( tiles, win_tile, is_tsumo=is_tsumo, is_riichi=is_riichi, is_dealer=is_dealer, is_ippatsu=False, is_rinshan=is_rinshan, is_chankan=False, is_haitei=False, is_houtei=False, is_daburu_riichi=False, is_nagashi_mangan=False, is_tenhou=False, is_renhou=False, is_chiihou=False, open_sets=open_sets, dora_indicators=dora_indicators, called_kan_indices=None, player_wind=player_wind, round_wind=round_wind) logger.info(result) melds = self.table.players[p].melds result = self.hand_calculator.estimate_hand_value( tiles, win_tile, melds=melds, dora_indicators=dora_indicators, config=HandConfig(is_tsumo=is_tsumo, is_riichi=is_riichi, player_wind=player_wind, round_wind=round_wind, is_rinshan=is_rinshan)) logger.info(result) break # if not win, we do the followings if not self.table.players[0].in_riichi: # choose a discard tile for playe `p` discarded_tile = self.discard_tile(0) # see if we can call riichi if self.table.players[0].can_call_riichi(): logger.info("player {} call riichi.".format(0)) self.table.players[0].in_riichi = True else: # if riichi, we have to discard whatever we draw discarded_tile = drawn_tile # remove the tile from player's hand self.table.players[0].tiles.remove(discarded_tile) logger.debug("player {} discards {} after kan".format( 0, discarded_tile)) logger.debug("\tclosed hand: %s" % self.table.players[0].closed_hand) logger.debug("\topen hand: %s" % self.table.players[0].open_hand_34_tiles) logger.debug("\tmeld tiles: %s" % self.table.players[0].meld_tiles) # we had to add it to discards, to calculate remaining tiles correctly # drawn tile is not the one drawn from rinshan, but # the one previously discarded by player `p` self.table.add_discarded_tile(0, discarded_tile, True, previous_drawn_tile) # after program discarding a card, next player is 1 p = 1 continue else: # ryuukyoku logger.debug("Rinshan empty. Ryuukyoku!") break elif meld: # pon, chi logger.debug("player 0 %s from %d: %s" % (meld.type, p, meld)) player_seat = 0 # DEBUG: we change the add_called_meld method, delete the # part that changes self.table.count_of_remaining_tiles self.table.add_called_meld(player_seat, meld) # Equivalently, program draws the tile discarded by opponent self.table.players[0].draw_tile(tile) # check p win if self.check_win(0): logger.info("player {} wins (by {})!".format( 0, meld.type)) tiles = self.table.players[p].tiles win_tile = tile (is_tsumo, is_riichi, is_dealer, open_sets, dora_indicators, player_wind, round_wind) = self.check_status(p) ## TODO: Wait to be finished! result = self.finished_hand.estimate_hand_value( tiles, win_tile, is_tsumo=is_tsumo, is_riichi=is_riichi, is_dealer=is_dealer, is_ippatsu=False, is_rinshan=False, is_chankan=False, is_haitei=False, is_houtei=False, is_daburu_riichi=False, is_nagashi_mangan=False, is_tenhou=False, is_renhou=False, is_chiihou=False, open_sets=open_sets, dora_indicators=dora_indicators, called_kan_indices=None, player_wind=player_wind, round_wind=round_wind) logger.info(result) melds = self.table.players[p].melds result = self.hand_calculator.estimate_hand_value( tiles, win_tile, melds=melds, dora_indicators=dora_indicators, config=HandConfig(is_tsumo=is_tsumo, is_riichi=is_riichi, player_wind=player_wind, round_wind=round_wind)) logger.info(result) break # if not win, we do the followings if not self.table.players[0].in_riichi: # choose a discard tile for playe `p` discarded_tile = self.discard_tile(0) # see if we can call riichi if self.table.players[0].can_call_riichi(): logger.debug("player {} call riichi.".format(0)) self.table.players[0].in_riichi = True else: # if riichi, we can not call meld raise ("Riichi player can not call meld!") # remove the tile from player's hand self.table.players[0].tiles.remove(discarded_tile) # discarded tile added to table self.table.add_discarded_tile(0, discarded_tile, True, previous_drawn_tile) logger.debug("player {} discards {} after {}".format( 0, discarded_tile, meld.type)) logger.debug("\tclosed hand: %s" % self.table.players[0].closed_hand) logger.debug("\topen hand: %s" % self.table.players[0].open_hand_34_tiles) logger.debug("\tmeld tiles: %s" % self.table.players[0].meld_tiles) # after program discarding a card, next player is 1 p = 1 continue # we had to add it to discards, to calculate remaining tiles correctly self.table.add_discarded_tile(p, discarded_tile_tmp, is_tsumogiri, drawn_tile_tmp) # next player p = (p + 1) % 4 logger.debug("[after]:%s,%s" % (self.table.count_of_remaining_tiles, len(self.table.remaining_tiles))) # output results logger.debug('\n') for p in range(4): logger.info("\tPlayer %d: %s (%s)" % (p, TilesConverter.to_one_line_string( self.table.players[p].tiles), TilesConverter.to_one_line_string( self.table.players[p].closed_hand))) ## TODO: finish this part! def check_status(self, p, is_tsumo=False): """ We want to check the status of player `p` :param is_tsumo: :param is_riichi: :param is_dealer: :param is_ippatsu: :param is_rinshan: :param is_chankan: :param is_haitei: :param is_houtei: :param is_tenhou: :param is_renhou: :param is_chiihou: :param is_daburu_riichi: :param is_nagashi_mangan: :param open_sets: array of array with open sets in 34-tile format :param dora_indicators: array of tiles in 136-tile format :param called_kan_indices: array of tiles in 136-tile format :param player_wind: index of player wind :param round_wind: index of round wind """ is_tsumo = is_tsumo is_riichi = self.table.players[p].in_riichi is_dealer = (self.table.dealer_seat == p) open_sets = [ meld.tiles for meld in self.table.players[p].melds if meld.opened == True ] open_sets = list(map(lambda lst: [l // 4 for l in lst], open_sets)) # convert to 34 format dora_indicators = self.table.dora_indicators player_wind = self.table.players[p].player_wind round_wind = self.table.round_wind return (is_tsumo, is_riichi, is_dealer, open_sets, dora_indicators, player_wind, round_wind) def _check2(self): self.sim_game() def _check(self): BW_riichi, BW_stealing, R, Wpet_riichi, Wpet_stealing = self.get_WPET( 500) print("BW riichi: %s" % BW_riichi) print("BW stealing: %s" % BW_stealing) print("R: %s" % R) print("WPET riichi: %s" % Wpet_riichi) print("WPET stealing: %s" % Wpet_stealing) import matplotlib.pyplot as plt plt.plot(range(19), Wpet_riichi, '*-r', range(19), Wpet_stealing, 'o-b')
def test_is_chuuren_poutou(self): hand = HandCalculator() tiles = self._string_to_34_array(man="11112345678999") self.assertTrue( self.config.chuuren_poutou.is_condition_met(self._hand(tiles))) tiles = self._string_to_34_array(pin="11122345678999") self.assertTrue( self.config.chuuren_poutou.is_condition_met(self._hand(tiles))) tiles = self._string_to_34_array(sou="11123345678999") self.assertTrue( self.config.chuuren_poutou.is_condition_met(self._hand(tiles))) tiles = self._string_to_34_array(sou="11123445678999") self.assertTrue( self.config.chuuren_poutou.is_condition_met(self._hand(tiles))) tiles = self._string_to_34_array(sou="11123455678999") self.assertTrue( self.config.chuuren_poutou.is_condition_met(self._hand(tiles))) tiles = self._string_to_34_array(sou="11123456678999") self.assertTrue( self.config.chuuren_poutou.is_condition_met(self._hand(tiles))) tiles = self._string_to_34_array(sou="11123456778999") self.assertTrue( self.config.chuuren_poutou.is_condition_met(self._hand(tiles))) tiles = self._string_to_34_array(sou="11123456788999") self.assertTrue( self.config.chuuren_poutou.is_condition_met(self._hand(tiles))) tiles = self._string_to_34_array(sou="11123456789999") self.assertTrue( self.config.chuuren_poutou.is_condition_met(self._hand(tiles))) tiles = self._string_to_136_array(man="11123456789999") win_tile = self._string_to_136_tile(man="1") result = hand.estimate_hand_value(tiles, win_tile) self.assertEqual(result.error, None) self.assertEqual(result.han, 13) self.assertEqual(result.fu, 40) self.assertEqual(len(result.yaku), 1) daburi = [ ["11122345678999", "2"], ["11123456789999", "9"], ["11112345678999", "1"], ] for hand_tiles, win_tile in daburi: tiles = self._string_to_136_array(man=hand_tiles) win_tile = self._string_to_136_tile(man=win_tile) result = hand.estimate_hand_value(tiles, win_tile) self.assertEqual(result.error, None) self.assertEqual(result.han, 26) self.assertEqual(len(result.yaku), 1) tiles = self._string_to_136_array(pin="111234566789999") win_tile = self._string_to_136_tile(pin="3") melds = [self._make_meld(Meld.KAN, pin="9999", is_open=False)] result = hand.estimate_hand_value(tiles, win_tile, melds=melds) self.assertEqual(result.error, None) self.assertEqual(result.han, 6) self.assertEqual(result.fu, 70) self.assertEqual(len(result.yaku), 1)
class ImplementationAI(InterfaceAI): version = '0.4.0' agari = None shanten_calculator = None defence = None riichi = None hand_divider = None finished_hand = None shanten = 7 ukeire = 0 ukeire_second = 0 in_defence = False waiting = None current_strategy = None last_discard_option = None hand_cache = {} gpparams = {} def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() self.shanten_calculator = Shanten() self.defence = DefenceHandler(player) self.riichi = Riichi(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.hand_builder = HandBuilder(player, self) self.erase_state() def erase_state(self): self.shanten = 7 self.ukeire = 0 self.ukeire_second = 0 self.in_defence = False self.waiting = None self.current_strategy = None self.last_discard_option = None self.hand_cache = {} def load_params(self, params): self.gpparams = params 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 draw_tile(self, tile_136): self.determine_strategy(self.player.tiles) def discard_tile(self, discard_tile, print_log=True): # we called meld and we had discard tile that we wanted to discard if discard_tile is not None: if not self.last_discard_option: return discard_tile return self.hand_builder.process_discard_option( self.last_discard_option, self.player.closed_hand, True) return self.hand_builder.discard_tile(self.player.tiles, self.player.closed_hand, self.player.melds, print_log) def try_to_call_meld(self, tile_136, is_kamicha_discard, remaining_tiles): tiles_136_previous = self.player.tiles[:] tiles_136 = tiles_136_previous + [tile_136] self.determine_strategy(tiles_136) if not self.current_strategy: return None, None tiles_34_previous = TilesConverter.to_34_array(tiles_136_previous) previous_shanten, _ = self.hand_builder.calculate_shanten( tiles_34_previous, self.player.meld_34_tiles) if previous_shanten == Shanten.AGARI_STATE and not self.current_strategy.can_meld_into_agari( ): return None, None meld, discard_option = self.current_strategy.try_to_call_meld( tile_136, is_kamicha_discard, tiles_136, remaining_tiles) if discard_option: self.last_discard_option = discard_option DecisionsLogger.debug( log.MELD_CALL, 'Try to call meld', context=[ 'Hand: {}'.format( self.player.format_hand_for_print(tile_136)), 'Meld: {}'.format(meld), 'Discard after meld: {}'.format(discard_option) ]) return meld, discard_option def determine_strategy(self, tiles_136): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False old_strategy = self.current_strategy self.current_strategy = None # order is important, we add strategies with the highest priority first strategies = [] if self.player.table.has_open_tanyao: strategies.append( TanyaoStrategy(BaseStrategy.TANYAO, self.player, self.gpparams)) strategies.append( YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player, self.gpparams)) strategies.append( HonitsuStrategy(BaseStrategy.HONITSU, self.player, self.gpparams)) strategies.append( ChinitsuStrategy(BaseStrategy.CHINITSU, self.player, self.gpparams)) strategies.append( ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, self.player, self.gpparams)) strategies.append( FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player, self.gpparams)) for strategy in strategies: if strategy.should_activate_strategy(tiles_136): self.current_strategy = strategy break if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: DecisionsLogger.debug( log.STRATEGY_ACTIVATE, context=self.current_strategy, ) if not self.current_strategy and old_strategy: DecisionsLogger.debug(log.STRATEGY_DROP, context=old_strategy) return self.current_strategy and True or False def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False, is_tsumo=False): """ :param win_tile: 34 tile format :param tiles: :param call_riichi: :param is_tsumo :return: """ win_tile *= 4 # we don't need to think, that our waiting is aka dora if win_tile in AKA_DORA_LIST: win_tile += 1 if not tiles: tiles = copy.copy(self.player.tiles) tiles += [win_tile] config = HandConfig( is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind_tile, has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao, is_tsumo=is_tsumo, ) result = self.finished_hand.estimate_hand_value( tiles, win_tile, self.player.melds, self.player.table.dora_indicators, config) return result def should_call_riichi(self): return self.riichi.should_call_riichi() 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 def should_call_win(self, tile, enemy_seat): return True def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ if self.defence.should_go_to_defence_mode(): self.in_defence = True DecisionsLogger.debug(log.DEFENCE_ACTIVATE) @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:] def _calculate_shanten_for_kan(self, tiles, closed_hand_tiles, melds): previous_results, previous_shanten = self.hand_builder.find_discard_options( tiles, closed_hand_tiles, melds) previous_results = [ x for x in previous_results if x.shanten == previous_shanten ] # it is possible that we don't have results here # when we are in agari state (but without yaku) if not previous_results: return None, None previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire return previous_shanten, previous_waits_cnt
from mahjong.hand_calculating.hand import HandCalculator from mahjong.meld import Meld from mahjong.hand_calculating.hand_config import HandConfig from mahjong.shanten import Shanten from mahjong.tile import TilesConverter calculator = HandCalculator() # useful helper def print_hand_result(hand_result): print(hand_result.han, hand_result.fu) print(hand_result.cost['main']) print(hand_result.yaku) for fu_item in hand_result.fu_details: print(fu_item) print('') #################################################################### # Tanyao hand by ron # #################################################################### # we had to use all 14 tiles in that array tiles = TilesConverter.string_to_136_array(man='22444', pin='333567', sou='444') win_tile = TilesConverter.string_to_136_array(sou='4')[0] result = calculator.estimate_hand_value(tiles, win_tile) print_hand_result(result)
class ImplementationAI(InterfaceAI): version = '0.4.0' agari = None shanten_calculator = None defence = None riichi = None hand_divider = None finished_hand = None shanten = 7 ukeire = 0 ukeire_second = 0 in_defence = False waiting = None current_strategy = None last_discard_option = None hand_cache = {} def __init__(self, player): super(ImplementationAI, self).__init__(player) self.agari = Agari() self.shanten_calculator = Shanten() self.defence = DefenceHandler(player) self.riichi = Riichi(player) self.hand_divider = HandDivider() self.finished_hand = HandCalculator() self.hand_builder = HandBuilder(player, self) self.erase_state() def erase_state(self): self.shanten = 7 self.ukeire = 0 self.ukeire_second = 0 self.in_defence = False self.waiting = None self.current_strategy = None self.last_discard_option = None self.hand_cache = {} 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 draw_tile(self, tile_136): self.determine_strategy(self.player.tiles) def discard_tile(self, discard_tile, print_log=True): # we called meld and we had discard tile that we wanted to discard if discard_tile is not None: if not self.last_discard_option: return discard_tile return self.hand_builder.process_discard_option(self.last_discard_option, self.player.closed_hand, True) return self.hand_builder.discard_tile( self.player.tiles, self.player.closed_hand, self.player.melds, print_log ) 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 determine_strategy(self, tiles_136): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False old_strategy = self.current_strategy self.current_strategy = None # order is important, we add strategies with the highest priority first strategies = [] if self.player.table.has_open_tanyao: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) strategies.append(YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player)) strategies.append(HonitsuStrategy(BaseStrategy.HONITSU, self.player)) strategies.append(ChinitsuStrategy(BaseStrategy.CHINITSU, self.player)) strategies.append(ChiitoitsuStrategy(BaseStrategy.CHIITOITSU, self.player)) strategies.append(FormalTempaiStrategy(BaseStrategy.FORMAL_TEMPAI, self.player)) for strategy in strategies: if strategy.should_activate_strategy(tiles_136): self.current_strategy = strategy break if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: DecisionsLogger.debug( log.STRATEGY_ACTIVATE, context=self.current_strategy, ) if not self.current_strategy and old_strategy: DecisionsLogger.debug(log.STRATEGY_DROP, context=old_strategy) return self.current_strategy and True or False def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False, is_tsumo=False): """ :param win_tile: 34 tile format :param tiles: :param call_riichi: :param is_tsumo :return: """ win_tile *= 4 # we don't need to think, that our waiting is aka dora if win_tile in AKA_DORA_LIST: win_tile += 1 if not tiles: tiles = copy.copy(self.player.tiles) tiles += [win_tile] config = HandConfig( is_riichi=call_riichi, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind_tile, has_aka_dora=self.player.table.has_aka_dora, has_open_tanyao=self.player.table.has_open_tanyao, is_tsumo=is_tsumo, ) result = self.finished_hand.estimate_hand_value(tiles, win_tile, self.player.melds, self.player.table.dora_indicators, config) return result def should_call_riichi(self): return self.riichi.should_call_riichi() 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 def should_call_win(self, tile, enemy_seat): return True def enemy_called_riichi(self, enemy_seat): """ After enemy riichi we had to check will we fold or not it is affect open hand decisions :return: """ if self.defence.should_go_to_defence_mode(): self.in_defence = True DecisionsLogger.debug(log.DEFENCE_ACTIVATE) @property def enemy_players(self): """ Return list of players except our bot """ return self.player.table.players[1:] def _calculate_shanten_for_kan(self, tiles, closed_hand_tiles, melds): previous_results, previous_shanten = self.hand_builder.find_discard_options( tiles, closed_hand_tiles, melds ) previous_results = [x for x in previous_results if x.shanten == previous_shanten] # it is possible that we don't have results here # when we are in agari state (but without yaku) if not previous_results: return None, None previous_waits_cnt = sorted(previous_results, key=lambda x: -x.ukeire)[0].ukeire return previous_shanten, previous_waits_cnt