def test_is_suukantsu(self): hand = FinishedHand() tiles = self._string_to_34_array(sou='111333', man='222', pin='44555') called_kan_indices = [ self._string_to_34_tile(sou='1'), self._string_to_34_tile(sou='3'), self._string_to_34_tile(pin='5'), self._string_to_34_tile(man='2') ] self.assertTrue( hand.is_suukantsu(self._hand(tiles, 0), called_kan_indices)) tiles = self._string_to_136_array(sou='111333', man='222', pin='44555') win_tile = self._string_to_136_tile(pin='4') open_sets = [ self._string_to_open_34_set(sou='111'), self._string_to_open_34_set(sou='333') ] called_kan_indices = [ self._string_to_136_tile(sou='1'), self._string_to_136_tile(sou='3'), self._string_to_136_tile(pin='5'), self._string_to_136_tile(man='2') ] result = hand.estimate_hand_value( tiles, win_tile, open_sets=open_sets, called_kan_indices=called_kan_indices) self.assertEqual(result['error'], None) self.assertEqual(result['han'], 13) self.assertEqual(result['fu'], 80) self.assertEqual(len(result['hand_yaku']), 1)
def test_is_kokushi(self): hand = FinishedHand() tiles = self._string_to_34_array(sou='119', man='19', pin='19', honors='1234567') self.assertTrue(hand.is_kokushi(tiles)) tiles = self._string_to_136_array(sou='119', man='19', pin='19', honors='1234567') win_tile = self._string_to_136_tile(sou='9') result = hand.estimate_hand_value(tiles, win_tile) self.assertEqual(result['error'], None) self.assertEqual(result['han'], 13) self.assertEqual(result['fu'], 0) self.assertEqual(len(result['hand_yaku']), 1) 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) self.assertEqual(result['error'], None) self.assertEqual(result['han'], 26) self.assertEqual(result['fu'], 0) self.assertEqual(len(result['hand_yaku']), 1)
def __init__(self, clients): self.tiles = [] self.dead_wall = [] self.dora_indicators = [] self.clients = clients self._set_client_names() self.agari = Agari() self.finished_hand = FinishedHand()
def test_is_tenhou(self): hand = FinishedHand() tiles = self._string_to_136_array(sou='123444', man='234456', pin='66') win_tile = self._string_to_136_tile(sou='4') result = hand.estimate_hand_value(tiles, win_tile, is_tenhou=True) self.assertEqual(result['error'], None) self.assertEqual(result['han'], 13) self.assertEqual(result['fu'], 40) self.assertEqual(len(result['hand_yaku']), 1)
def __init__(self, player): super(MainAI, self).__init__(player) self.agari = Agari() self.shanten = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = FinishedHand() self.previous_shanten = 7 self.current_strategy = None self.waiting = [] self.in_defence = False
def test_is_chinroto(self): hand = FinishedHand() tiles = self._string_to_34_array(sou='111999', man='111999', pin='99') self.assertTrue(hand.is_chinroto(self._hand(tiles, 0))) tiles = self._string_to_136_array(sou='111222', man='111999', pin='99') win_tile = self._string_to_136_tile(pin='9') result = hand.estimate_hand_value(tiles, win_tile) self.assertEqual(result['error'], None) self.assertEqual(result['han'], 26) self.assertEqual(result['fu'], 60) self.assertEqual(len(result['hand_yaku']), 1)
def test_is_daisuushi(self): hand = FinishedHand() tiles = self._string_to_34_array(sou='22', honors='111222333444') self.assertTrue(hand.is_daisuushi(self._hand(tiles, 0))) tiles = self._string_to_136_array(sou='22', honors='111222333444') win_tile = self._string_to_136_tile(honors='4') result = hand.estimate_hand_value(tiles, win_tile) self.assertEqual(result['error'], None) self.assertEqual(result['han'], 26) self.assertEqual(result['fu'], 60) self.assertEqual(len(result['hand_yaku']), 1)
def test_is_ryuisou(self): hand = FinishedHand() tiles = self._string_to_34_array(sou='22334466888', honors='666') self.assertTrue(hand.is_ryuisou(self._hand(tiles, 0))) tiles = self._string_to_136_array(sou='22334466888', honors='666') win_tile = self._string_to_136_tile(honors='6') 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['hand_yaku']), 1)
def test_is_daisangen(self): hand = FinishedHand() tiles = self._string_to_34_array(sou='123', man='22', honors='555666777') self.assertTrue(hand.is_daisangen(self._hand(tiles, 0))) tiles = self._string_to_136_array(sou='123', man='22', honors='555666777') win_tile = self._string_to_136_tile(honors='7') result = hand.estimate_hand_value(tiles, win_tile) self.assertEqual(result['error'], None) self.assertEqual(result['han'], 13) self.assertEqual(result['fu'], 50) self.assertEqual(len(result['hand_yaku']), 1)
def test_is_chuuren_poutou(self): hand = FinishedHand() tiles = self._string_to_34_array(man='11122345678999') self.assertTrue(hand.is_chuuren_poutou(self._hand(tiles, 0))) tiles = self._string_to_34_array(pin='11123345678999') self.assertTrue(hand.is_chuuren_poutou(self._hand(tiles, 0))) tiles = self._string_to_34_array(sou='11123456678999') self.assertTrue(hand.is_chuuren_poutou(self._hand(tiles, 0))) tiles = self._string_to_34_array(sou='11123456678999') self.assertTrue(hand.is_chuuren_poutou(self._hand(tiles, 0))) tiles = self._string_to_34_array(sou='11123456678999') self.assertTrue(hand.is_chuuren_poutou(self._hand(tiles, 0))) tiles = self._string_to_34_array(sou='11123456789999') self.assertTrue(hand.is_chuuren_poutou(self._hand(tiles, 0))) 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['hand_yaku']), 1) tiles = self._string_to_136_array(man='11122345678999') win_tile = self._string_to_136_tile(man='2') result = hand.estimate_hand_value(tiles, win_tile) self.assertEqual(result['error'], None) self.assertEqual(result['han'], 26) self.assertEqual(result['fu'], 50) self.assertEqual(len(result['hand_yaku']), 1)
class CheckWaiting(object): def __init__(self): self.finished_hand = FinishedHand() def check(self, hand, win_tile, is_tsumo=False, is_riichi=False, is_dealer=False, 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=None, dora_indicators=None, called_kan_indices=None, player_wind=None, round_wind=None): result = self.finished_hand.estimate_hand_value( hand, win_tile, is_tsumo=is_tsumo, is_riichi=is_riichi, is_dealer=is_dealer, is_ippatsu=is_ippatsu, is_rinshan=is_rinshan, is_chankan=is_chankan, is_haitei=is_haitei, is_houtei=is_houtei, is_daburu_riichi=is_daburu_riichi, is_nagashi_mangan=is_nagashi_mangan, is_tenhou=is_tenhou, is_renhou=is_renhou, is_chiihou=is_chiihou, open_sets=open_sets, dora_indicators=dora_indicators, called_kan_indices=called_kan_indices, player_wind=player_wind, round_wind=round_wind) return result
def test_is_suuankou(self): hand = FinishedHand() tiles = self._string_to_34_array(sou='111444', man='333', pin='44555') win_tile = self._string_to_136_tile(sou='4') self.assertTrue(hand.is_suuankou(win_tile, self._hand(tiles, 0), True)) self.assertFalse( hand.is_suuankou(win_tile, self._hand(tiles, 0), 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, is_tsumo=True) self.assertEqual(result['error'], None) self.assertEqual(result['han'], 13) self.assertEqual(result['fu'], 50) self.assertEqual(len(result['hand_yaku']), 1) result = hand.estimate_hand_value(tiles, win_tile, 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, is_tsumo=True) self.assertEqual(result['error'], None) self.assertEqual(result['han'], 26) self.assertEqual(result['fu'], 50) self.assertEqual(len(result['hand_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, is_tsumo=False) self.assertEqual(result['error'], None) self.assertEqual(result['han'], 26) self.assertEqual(result['fu'], 50) self.assertEqual(len(result['hand_yaku']), 1)
def test_is_tsuisou(self): hand = FinishedHand() tiles = self._string_to_34_array(honors='11122233366677') self.assertTrue(hand.is_tsuisou(self._hand(tiles, 0))) tiles = self._string_to_34_array(honors='11223344556677') self.assertTrue(hand.is_tsuisou(self._hand(tiles, 0))) tiles = self._string_to_34_array(honors='1133445577', pin='88', sou='11') self.assertFalse(hand.is_tsuisou(self._hand(tiles, 0))) tiles = self._string_to_136_array(honors='11223344556677') win_tile = self._string_to_136_tile(honors='7') result = hand.estimate_hand_value(tiles, win_tile) self.assertEqual(result['error'], None) self.assertEqual(result['han'], 13) self.assertEqual(result['fu'], 25) self.assertEqual(len(result['hand_yaku']), 1)
def test_calculate_scores_and_ron(self): hand = FinishedHand() result = hand.calculate_scores(han=1, fu=30, is_tsumo=False, is_dealer=False) self.assertEqual(result['main'], 1000) result = hand.calculate_scores(han=1, fu=110, is_tsumo=False, is_dealer=False) self.assertEqual(result['main'], 3600) result = hand.calculate_scores(han=2, fu=30, is_tsumo=False, is_dealer=False) self.assertEqual(result['main'], 2000) result = hand.calculate_scores(han=3, fu=30, is_tsumo=False, is_dealer=False) self.assertEqual(result['main'], 3900) result = hand.calculate_scores(han=4, fu=30, is_tsumo=False, is_dealer=False) self.assertEqual(result['main'], 7700) result = hand.calculate_scores(han=4, fu=40, is_tsumo=False, is_dealer=False) self.assertEqual(result['main'], 8000) result = hand.calculate_scores(han=4, fu=40, is_tsumo=False, is_dealer=True) self.assertEqual(result['main'], 12000) result = hand.calculate_scores(han=5, fu=0, is_tsumo=False, is_dealer=False) self.assertEqual(result['main'], 8000) result = hand.calculate_scores(han=6, fu=0, is_tsumo=False, is_dealer=False) self.assertEqual(result['main'], 12000) result = hand.calculate_scores(han=8, fu=0, is_tsumo=False, is_dealer=False) self.assertEqual(result['main'], 16000) result = hand.calculate_scores(han=11, fu=0, is_tsumo=False, is_dealer=False) self.assertEqual(result['main'], 24000) result = hand.calculate_scores(han=13, fu=0, is_tsumo=False, is_dealer=False) self.assertEqual(result['main'], 32000) result = hand.calculate_scores(han=26, fu=0, is_tsumo=False, is_dealer=False) self.assertEqual(result['main'], 64000)
def test_calculate_scores_and_tsumo_by_dealer(self): hand = FinishedHand() result = hand.calculate_scores(han=1, fu=30, is_tsumo=True, is_dealer=True) self.assertEqual(result['main'], 500) self.assertEqual(result['additional'], 500) result = hand.calculate_scores(han=3, fu=30, is_tsumo=True, is_dealer=True) self.assertEqual(result['main'], 2000) self.assertEqual(result['additional'], 2000) result = hand.calculate_scores(han=4, fu=30, is_tsumo=True, is_dealer=True) self.assertEqual(result['main'], 3900) self.assertEqual(result['additional'], 3900) result = hand.calculate_scores(han=5, fu=0, is_tsumo=True, is_dealer=True) self.assertEqual(result['main'], 4000) self.assertEqual(result['additional'], 4000) result = hand.calculate_scores(han=6, fu=0, is_tsumo=True, is_dealer=True) self.assertEqual(result['main'], 6000) self.assertEqual(result['additional'], 6000) result = hand.calculate_scores(han=8, fu=0, is_tsumo=True, is_dealer=True) self.assertEqual(result['main'], 8000) self.assertEqual(result['additional'], 8000) result = hand.calculate_scores(han=11, fu=0, is_tsumo=True, is_dealer=True) self.assertEqual(result['main'], 12000) self.assertEqual(result['additional'], 12000) result = hand.calculate_scores(han=13, fu=0, is_tsumo=True, is_dealer=True) self.assertEqual(result['main'], 16000) self.assertEqual(result['additional'], 16000) result = hand.calculate_scores(han=26, fu=0, is_tsumo=True, is_dealer=True) self.assertEqual(result['main'], 32000) self.assertEqual(result['additional'], 32000)
def __init__(self): self.finished_hand = FinishedHand()
class GameManager(object): """ Allow to play bots between each other To have a metrics how new version plays agains old versions """ tiles = [] dead_wall = [] clients = [] dora_indicators = [] dealer = None current_client = None round_number = 0 honba_sticks = 0 riichi_sticks = 0 _unique_dealers = 0 def __init__(self, clients): self.tiles = [] self.dead_wall = [] self.dora_indicators = [] self.clients = clients self._set_client_names() self.agari = Agari() self.finished_hand = FinishedHand() def init_game(self): """ Initial of the game. Clients random placement and dealer selection """ shuffle(self.clients, shuffle_seed) for i in range(0, len(self.clients)): self.clients[i].position = i dealer = randint(0, 3) self.set_dealer(dealer) for client in self.clients: client.player.scores = 25000 self._unique_dealers = 1 def init_round(self): # each round should have personal seed global seed_value seed_value = random() self.tiles = [i for i in range(0, 136)] # need to change random function in future shuffle(self.tiles, shuffle_seed) self.dead_wall = self._cut_tiles(14) self.dora_indicators.append(self.dead_wall[8]) 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.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.round_number, 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 x 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.init_hand(client.player.tiles) logger.info('Seed: {0}'.format(shuffle_seed())) logger.info('Dealer: {0}'.format(self.dealer)) logger.info('Wind: {0}. Riichi sticks: {1}. Honba sticks: {2}'.format( self._unique_dealers, self.riichi_sticks, self.honba_sticks )) logger.info('Players: {0}'.format(self.players_sorted_by_scores())) def play_round(self): continue_to_play = True while continue_to_play: client = self._get_current_client() in_tempai = client.player.in_tempai tile = self._cut_tiles(1)[0] # we don't need to add tile to the hand when we are in riichi if client.player.in_riichi: tiles = client.player.tiles + [tile] else: client.draw_tile(tile) tiles = client.player.tiles is_win = self.agari.is_agari(TilesConverter.to_34_array(tiles)) # win by tsumo after tile draw if is_win: result = self.process_the_end_of_the_round(tiles=client.player.tiles, win_tile=tile, winner=client, loser=None, is_tsumo=True) return result # if not in riichi, let's decide what tile to discard if not client.player.in_riichi: tile = client.discard_tile() in_tempai = client.player.in_tempai # after tile discard let's check all other players can they win or not # at this tile for other_client in self.clients: # there is no need to check the current client if other_client == client: continue # let's store other players discards other_client.enemy_discard(other_client.position - client.position, tile) # TODO support multiple ron if self.can_call_ron(other_client, tile): # the end of the round result = self.process_the_end_of_the_round(tiles=other_client.player.tiles, win_tile=tile, winner=other_client, loser=client, is_tsumo=False) return result # if there is no challenger to ron, let's check can we call riichi with tile discard or not if in_tempai and client.player.can_call_riichi(): self.call_riichi(client) self.current_client = self._move_position(self.current_client) # 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 play_game(self, total_results): """ :param total_results: a dictionary with keys as client ids :return: game results """ logger.info('The start of the game') logger.info('') is_game_end = False self.init_game() played_rounds = 0 while not is_game_end: self.init_round() result = self.play_round() is_game_end = result['is_game_end'] loser = result['loser'] winner = result['winner'] if loser: total_results[loser.id]['lose_rounds'] += 1 if winner: total_results[winner.id]['win_rounds'] += 1 for client in self.clients: if client.player.in_riichi: total_results[client.id]['riichi_rounds'] += 1 played_rounds += 1 self.recalculate_players_position() logger.info('Final Scores: {0}'.format(self.players_sorted_by_scores())) logger.info('The end of the game') return {'played_rounds': played_rounds} 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 def can_call_ron(self, client, win_tile): if not client.player.in_tempai or not client.player.in_riichi: return False tiles = client.player.tiles is_ron = self.agari.is_agari(TilesConverter.to_34_array(tiles + [win_tile])) return is_ron def call_riichi(self, client): client.player.in_riichi = True client.player.scores -= 1000 self.riichi_sticks += 1 who_called_riichi = client.position for client in self.clients: client.enemy_riichi(who_called_riichi - client.position) logger.info('Riichi: {0} - 1,000'.format(client.player.name)) 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 = dealer - x # first move should be dealer's move self.current_client = dealer def process_the_end_of_the_round(self, tiles, win_tile, winner, loser, is_tsumo): """ Increment a round number and do a scores calculations """ if winner: logger.info('{0}: {1} + {2}'.format( is_tsumo and 'Tsumo' or 'Ron', TilesConverter.to_one_line_string(tiles), TilesConverter.to_one_line_string([win_tile])), ) else: logger.info('Retake') is_game_end = False self.round_number += 1 if winner: hand_value = self.finished_hand.estimate_hand_value(tiles + [win_tile], win_tile, is_tsumo, winner.player.in_riichi, winner.player.is_dealer, False) if hand_value['cost']: hand_value = hand_value['cost']['main'] else: logger.error('Can\'t estimate a hand: {0}. Error: {1}'.format( TilesConverter.to_one_line_string(tiles + [win_tile]), hand_value['error'] )) hand_value = 1000 scores_to_pay = hand_value + self.honba_sticks * 300 riichi_bonus = self.riichi_sticks * 1000 self.riichi_sticks = 0 # if dealer won we need to increment honba sticks if winner.player.is_dealer: self.honba_sticks += 1 else: self.honba_sticks = 0 new_dealer = self._move_position(self.dealer) self.set_dealer(new_dealer) # win by ron if loser: win_amount = scores_to_pay + riichi_bonus winner.player.scores += win_amount loser.player.scores -= scores_to_pay logger.info('Win: {0} + {1:,d}'.format(winner.player.name, win_amount)) logger.info('Lose: {0} - {1:,d}'.format(loser.player.name, scores_to_pay)) # win by tsumo else: scores_to_pay /= 3 # will be changed when real hand calculation will be implemented # round to nearest 100. 333 -> 300 scores_to_pay = 100 * round(float(scores_to_pay) / 100) win_amount = scores_to_pay * 3 + riichi_bonus winner.player.scores += win_amount for client in self.clients: if client != winner: client.player.scores -= scores_to_pay logger.info('Win: {0} + {1:,d}'.format(winner.player.name, win_amount)) # retake else: tempai_users = 0 for client in self.clients: if client.player.in_tempai: tempai_users += 1 if tempai_users == 0 or tempai_users == 4: self.honba_sticks += 1 # no one in tempai, so deal should move if tempai_users == 0: new_dealer = self._move_position(self.dealer) self.set_dealer(new_dealer) 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 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) # if someone has negative scores, # we need to end the game for client in self.clients: if client.player.scores < 0: is_game_end = True # we have played all 8 winds, let's finish the game if self._unique_dealers > 8: is_game_end = True logger.info('') return { 'winner': winner, 'loser': loser, 'is_tsumo': is_tsumo, 'is_game_end': is_game_end } def players_sorted_by_scores(self): return sorted([i.player for i in self.clients], key=lambda x: x.scores, reverse=True) def _set_client_names(self): """ For better tests output """ names = ['Sato', 'Suzuki', 'Takahashi', 'Tanaka', 'Watanabe', 'Ito', 'Yamamoto', 'Nakamura', 'Kobayashi', 'Kato', 'Yoshida', 'Yamada'] for client in self.clients: name = names[randint(0, len(names) - 1)] names.remove(name) from mahjong.myAI.SLCNNPlayer import SLCNNPlayer print(isinstance(client.player.ai, SLCNNPlayer)) #if isinstance(client.player.ai, SLCNNPlayer): # client.player.name = "Suu" #else: # client.player.name = name def _get_current_client(self) -> Client: return self.clients[self.current_client] 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): """ loop 0 -> 1 -> 2 -> 3 -> 0 """ current_position += 1 if current_position > 3: current_position = 0 return current_position
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_calculate_scores_and_ron_by_dealer(self): hand = FinishedHand() result = hand.calculate_scores(han=1, fu=30, is_tsumo=False, is_dealer=True) self.assertEqual(result['main'], 1500) result = hand.calculate_scores(han=2, fu=30, is_tsumo=False, is_dealer=True) self.assertEqual(result['main'], 2900) result = hand.calculate_scores(han=3, fu=30, is_tsumo=False, is_dealer=True) self.assertEqual(result['main'], 5800) result = hand.calculate_scores(han=4, fu=30, is_tsumo=False, is_dealer=True) self.assertEqual(result['main'], 11600) result = hand.calculate_scores(han=5, fu=0, is_tsumo=False, is_dealer=True) self.assertEqual(result['main'], 12000) result = hand.calculate_scores(han=6, fu=0, is_tsumo=False, is_dealer=True) self.assertEqual(result['main'], 18000) result = hand.calculate_scores(han=8, fu=0, is_tsumo=False, is_dealer=True) self.assertEqual(result['main'], 24000) result = hand.calculate_scores(han=11, fu=0, is_tsumo=False, is_dealer=True) self.assertEqual(result['main'], 36000) result = hand.calculate_scores(han=13, fu=0, is_tsumo=False, is_dealer=True) self.assertEqual(result['main'], 48000) result = hand.calculate_scores(han=26, fu=0, is_tsumo=False, is_dealer=True) self.assertEqual(result['main'], 96000)
def parse_log(self, log_data, log_id): decoder = TenhouDecoder() finished_hand = FinishedHand() soup = BeautifulSoup(log_data, 'html.parser') elements = soup.find_all() settings.FIVE_REDS = True settings.OPEN_TANYAO = True total_hand = 0 successful_hand = 0 played_rounds = 0 dealer = 0 round_wind = EAST for tag in elements: if tag.name == 'go': game_rule_temp = int(tag.attrs['type']) # let's skip hirosima games hirosima = [177, 185, 241, 249] if game_rule_temp in hirosima: print('0,0') return # one round games skip_games = [2113] if game_rule_temp in skip_games: print('0,0') return no_red_five = [163, 167, 171, 175] if game_rule_temp in no_red_five: settings.FIVE_REDS = False no_open_tanyao = [167, 175] if game_rule_temp in no_open_tanyao: settings.OPEN_TANYAO = False if tag.name == 'taikyoku': dealer = int(tag.attrs['oya']) if tag.name == 'init': dealer = int(tag.attrs['oya']) seed = [int(i) for i in tag.attrs['seed'].split(',')] round_number = seed[0] if round_number < 4: round_wind = EAST elif 4 <= round_number < 8: round_wind = SOUTH elif 8 <= round_number < 12: round_wind = WEST else: round_wind = NORTH played_rounds += 1 if tag.name == 'agari': success = True winner = int(tag.attrs['who']) from_who = int(tag.attrs['fromwho']) closed_hand = [int(i) for i in tag.attrs['hai'].split(',')] ten = [int(i) for i in tag.attrs['ten'].split(',')] dora_indicators = [ int(i) for i in tag.attrs['dorahai'].split(',') ] if 'dorahaiura' in tag.attrs: dora_indicators += [ int(i) for i in tag.attrs['dorahaiura'].split(',') ] yaku_list = [] yakuman_list = [] if 'yaku' in tag.attrs: yaku_temp = [int(i) for i in tag.attrs['yaku'].split(',')] yaku_list = yaku_temp[::2] han = sum(yaku_temp[1::2]) else: yakuman_list = [ int(i) for i in tag.attrs['yakuman'].split(',') ] han = len(yakuman_list) * 13 fu = ten[0] cost = ten[1] melds = [] called_kan_indices = [] if 'm' in tag.attrs: for x in tag.attrs['m'].split(','): #message = '<N who={} m={}>'.format(tag.attrs['who'], x) # Modified[joseph]: added quotes to the params message = '<N who="{}" m="{}">'.format( tag.attrs['who'], x) meld = decoder.parse_meld(message) tiles = meld.tiles if len(tiles) == 4: called_kan_indices.append(tiles[0]) tiles = tiles[1:4] # closed kan if meld.from_who == 0: closed_hand.extend(tiles) else: melds.append(tiles) # Modified[joseph]: We need to turn tile in 136 format to 34 format here melds34 = [] for mld in melds: mld34 = [tile_136_to_34(m) for m in mld] melds34.append(mld34) hand = closed_hand if melds: hand += reduce(lambda z, y: z + y, melds) win_tile = int(tag.attrs['machi']) is_tsumo = winner == from_who is_riichi = 1 in yaku_list is_ippatsu = 2 in yaku_list is_chankan = 3 in yaku_list is_rinshan = 4 in yaku_list is_haitei = 5 in yaku_list is_houtei = 6 in yaku_list is_daburu_riichi = 21 in yaku_list is_dealer = winner == dealer is_renhou = 36 in yakuman_list is_tenhou = 37 in yakuman_list is_chiihou = 38 in yakuman_list dif = winner - dealer winds = [EAST, SOUTH, WEST, NORTH] player_wind = winds[dif] result = finished_hand.estimate_hand_value( hand, win_tile, is_tsumo=is_tsumo, is_riichi=is_riichi, is_dealer=is_dealer, is_ippatsu=is_ippatsu, is_rinshan=is_rinshan, is_chankan=is_chankan, is_haitei=is_haitei, is_houtei=is_houtei, is_daburu_riichi=is_daburu_riichi, is_tenhou=is_tenhou, is_renhou=is_renhou, is_chiihou=is_chiihou, round_wind=round_wind, player_wind=player_wind, called_kan_indices=called_kan_indices, open_sets=melds34, # melds, we use tile in 34 format dora_indicators=dora_indicators) if result['error']: logger.error('Error with hand calculation: {}'.format( result['error'])) calculated_cost = 0 success = False else: calculated_cost = result['cost'][ 'main'] + result['cost']['additional'] * 2 if success: if result['fu'] != fu: logger.error('Wrong fu: {} != {}'.format( result['fu'], fu)) success = False if result['han'] != han: logger.error('Wrong han: {} != {}'.format( result['han'], han)) success = False if cost != calculated_cost: logger.error('Wrong cost: {} != {}'.format( cost, calculated_cost)) success = False if not success: logger.error('http://e.mjv.jp/0/log/?{}'.format(log_id)) logger.error( 'http://tenhou.net/0/?log={}&tw={}&ts={}'.format( log_id, winner, played_rounds - 1)) logger.error('Winner: {}, Dealer: {}'.format( winner, dealer)) logger.error('Hand: {}'.format( TilesConverter.to_one_line_string(hand))) logger.error('Win tile: {}'.format( TilesConverter.to_one_line_string([win_tile]))) logger.error('Open sets: {}'.format(melds)) logger.error('Called kans: {}'.format( TilesConverter.to_one_line_string(called_kan_indices))) logger.error('Our results: {}'.format(result)) logger.error('Tenhou results: {}'.format(tag.attrs)) logger.error('Dora: {}'.format( TilesConverter.to_one_line_string(dora_indicators))) logger.error('') else: successful_hand += 1 print('Our results: {}'.format(result)) print("\t* dealer:{}, winner:{}, is_dealer:{}".format( dealer, winner, is_dealer)) print("\t* Calculated cost: {}".format(calculated_cost)) print('Tenhou results: {}'.format(tag.attrs)) print("-----------------------------------\n") total_hand += 1 print('{},{}'.format(successful_hand, total_hand))
class MainAI(BaseAI): version = '0.2.7' agari = None shanten = None defence = None hand_divider = None finished_hand = None previous_shanten = 7 in_defence = False waiting = None current_strategy = None def __init__(self, player): super(MainAI, self).__init__(player) self.agari = Agari() self.shanten = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = FinishedHand() self.previous_shanten = 7 self.current_strategy = None self.waiting = [] self.in_defence = False def erase_state(self): self.current_strategy = None self.in_defence = False def discard_tile(self): 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): 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) 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 return self.current_strategy.try_to_call_meld(tile, is_kamicha_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 settings.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.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, selected_tile, closed_hand): self.waiting = selected_tile.waiting self.player.ai.previous_shanten = selected_tile.shanten self.player.in_tempai = self.player.ai.previous_shanten == 0 return selected_tile.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] result = self.finished_hand.estimate_hand_value( tiles=tiles, win_tile=win_tile, is_tsumo=False, is_riichi=call_riichi, is_dealer=self.player.is_dealer, open_sets=self.player.open_hand_34_tiles, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind, dora_indicators=self.player.table.dora_indicators) return result def should_call_riichi(self): # empty waiting can be found in some cases if not self.waiting: 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 can_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 @property def valued_honors(self): return [ CHUN, HAKU, HATSU, self.player.table.round_wind, self.player.player_wind ]