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 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_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 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_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)
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_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)
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_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_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 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 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 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 ]