def test_auth_message(self): decoder = TenhouDecoder() message = '<HELO uname="%4E%6F%4E%61%6D%65" auth="20160318-54ebe070" ratingscale=""/>' result = decoder.parse_auth_string(message) self.assertEqual(result, '20160318-54ebe070')
def test_parse_initial_scores(): decoder = TenhouDecoder() message = ( '<INIT seed="0,2,3,0,1,126" ten="240,260,270,280" oya="3" hai="30,67,44,21,133,123,87,69,36,34,94,4,128"/>' ) values = decoder.parse_initial_values(message) assert values["scores"] == [240, 260, 270, 280]
def test_parse_called_chi(self): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="3" m="27031" />') self.assertEqual(meld.who, 3) self.assertEqual(meld.type, Meld.CHI) self.assertEqual(meld.tiles, [42, 44, 51])
def test_parse_initial_scores(self): decoder = TenhouDecoder() message = '<INIT seed="0,2,3,0,1,126" ten="240,260,270,280" oya="3" ' \ 'hai="30,67,44,21,133,123,87,69,36,34,94,4,128"/>' values = decoder.parse_initial_values(message) self.assertEqual(values['scores'], [240, 260, 270, 280])
def test_parse_initial_hand(self): decoder = TenhouDecoder() message = '<INIT seed="0,2,3,0,1,126" ten="250,250,250,250" oya="3" ' \ 'hai="30,67,44,21,133,123,87,69,36,34,94,4,128"/>' tiles = decoder.parse_initial_hand(message) self.assertEqual(len(tiles), 13)
def test_parse_called_pon(self): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="3" m="34314" />') self.assertEqual(meld.who, 3) self.assertEqual(meld.type, Meld.PON) self.assertEqual(meld.tiles, [89, 90, 91])
def test_parse_names_and_ranks(): decoder = TenhouDecoder() message = ( '<un n0="%4e%6f%4e%61%6d%65" n1="%6f%32%6f%32" n2="%73%68%69%6d%6d%6d%6d%6d"' ' n3="%e5%b7%9d%e6%b5%b7%e8%80%81" dan="0,7,12,1" ' 'rate="1500.00,1421.91,1790.94,1532.23" sx="m,m,m,m"/>') values = decoder.parse_names_and_ranks(message) assert values[0] == { "seat": 0, "name": "NoName", "rank": TenhouDecoder.RANKS[0] } assert values[1] == { "seat": 1, "name": "o2o2", "rank": TenhouDecoder.RANKS[7] } assert values[2] == { "seat": 2, "name": "shimmmmm", "rank": TenhouDecoder.RANKS[12] } assert values[3] == { "seat": 3, "name": "川海老", "rank": TenhouDecoder.RANKS[1] }
def test_auth_message(): decoder = TenhouDecoder() message = """<HELO uname="%4E%6F%4E%61%6D%65" PF4="9,45,1290.90,-5184.0,69,95,129,157,71,4303,831,830,33,1761"/>""" rating_string, _ = decoder.parse_hello_string(message) assert rating_string == "9,45,1290.90,-5184.0,69,95,129,157,71,4303,831,830,33,1761"
def test_decode_new_dora_indicator(): decoder = TenhouDecoder() message = '<DORA hai="125" />' result = decoder.parse_dora_indicator(message) assert result == 125
def test_parse_called_chakan(self): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="3" m="18547" />') self.assertEqual(meld.who, 3) self.assertEqual(meld.type, Meld.CHAKAN) self.assertEqual(meld.tiles, [48, 49, 50, 51])
def test_parse_initial_hand(): decoder = TenhouDecoder() message = ( '<INIT seed="0,2,3,0,1,126" ten="250,250,250,250" oya="3" hai="30,67,44,21,133,123,87,69,36,34,94,4,128"/>' ) tiles = decoder.parse_initial_hand(message) assert len(tiles) == 13
def test_parse_called_kan(self): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="3" m="13825" />') self.assertEqual(meld.who, 3) self.assertEqual(meld.type, Meld.KAN) self.assertEqual(meld.tiles, [52, 53, 54, 55])
def test_decode_new_dora_indicator(self): decoder = TenhouDecoder() message = '<DORA hai="125" />' result = decoder.parse_dora_indicator(message) self.assertEqual(result, 125)
def test_parse_called_pon(): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="3" m="34314" />') assert meld.who == 3 assert meld.type == MeldPrint.PON assert meld.opened is True assert meld.tiles == [89, 90, 91]
def test_generate_auth_token(self): client = TenhouDecoder() string = '20160318-54ebe070' self.assertEqual(client.generate_auth_token(string), '20160318-72b5ba21') string = '20160319-5b859bb3' self.assertEqual(client.generate_auth_token(string), '20160319-9bc528f3')
def test_parse_called_chi(): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="3" m="27031" />') assert meld.who == 3 assert meld.type == MeldPrint.CHI assert meld.opened is True assert meld.tiles == [42, 44, 51]
def test_parse_called_chi(self): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="3" m="27031" />') self.assertEqual(meld.who, 3) self.assertEqual(meld.type, Meld.CHI) self.assertEqual(meld.opened, True) self.assertEqual(meld.tiles, [42, 44, 51])
def test_parse_log_link(self): decoder = TenhouDecoder() message = '<TAIKYOKU oya="1" log="2016031911gm-0001-0000-381f693b"/>' game_id, position = decoder.parse_log_link(message) self.assertEqual(game_id, '2016031911gm-0001-0000-381f693b') self.assertEqual(position, 3)
def test_parse_called_chakan(self): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="3" m="18547" />') self.assertEqual(meld.who, 3) self.assertEqual(meld.type, Meld.CHANKAN) self.assertEqual(meld.opened, True) self.assertEqual(meld.tiles, [48, 49, 50, 51])
def test_parse_called_chakan(): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="3" m="18547" />') assert meld.who == 3 assert meld.type == MeldPrint.SHOUMINKAN assert meld.opened is True assert meld.tiles == [48, 49, 50, 51]
def test_parse_called_pon(self): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="3" m="34314" />') self.assertEqual(meld.who, 3) self.assertEqual(meld.type, Meld.PON) self.assertEqual(meld.opened, True) self.assertEqual(meld.tiles, [89, 90, 91])
def test_parse_called_opened_kan(self): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="3" m="13825" />') self.assertEqual(meld.who, 3) self.assertEqual(meld.from_who, 0) self.assertEqual(meld.type, Meld.KAN) self.assertEqual(meld.opened, True) self.assertEqual(meld.tiles, [52, 53, 54, 55])
def test_is_match_discard(self): decoder = TenhouDecoder() self.assertTrue(decoder.is_discarded_tile_message('<e107/>')) self.assertTrue(decoder.is_discarded_tile_message('<F107/>')) self.assertTrue(decoder.is_discarded_tile_message('<g107/>')) self.assertFalse(decoder.is_discarded_tile_message('<GO type="9" lobby="0" gpid=""/>')) self.assertFalse(decoder.is_discarded_tile_message('<FURITEN show="1" />'))
def test_parse_called_closed_kan(self): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="0" m="15872" />') self.assertEqual(meld.who, 0) self.assertEqual(meld.from_who, 0) self.assertEqual(meld.type, Meld.KAN) self.assertEqual(meld.opened, False) self.assertEqual(meld.tiles, [60, 61, 62, 63])
def __init__(self, log_url, stop_tag=None): log_id, player_position, needed_round = self._parse_url(log_url) log_content = self._download_log_content(log_id) rounds = self._parse_rounds(log_content) self.player_position = player_position self.round_content = rounds[needed_round] self.stop_tag = stop_tag self.decoder = TenhouDecoder()
def test_parse_called_opened_kan(): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="3" m="13825" />') assert meld.who == 3 assert meld.from_who == 0 assert meld.type == MeldPrint.KAN assert meld.opened is True assert meld.tiles == [52, 53, 54, 55]
def test_parse_called_closed_kan(): decoder = TenhouDecoder() meld = decoder.parse_meld('<N who="0" m="15872" />') assert meld.who == 0 assert meld.from_who == 0 assert meld.type == MeldPrint.KAN assert meld.opened is False assert meld.tiles == [60, 61, 62, 63]
def test_auth_message(self): decoder = TenhouDecoder() message = """<HELO uname="%4E%6F%4E%61%6D%65" auth="20160318-54ebe070" ratingscale="" PF4="9,45,1290.90,-5184.0,69,95,129,157,71,4303,831,830,33,1761"/>""" auth_string, rating_string, _ = decoder.parse_hello_string(message) self.assertEqual(auth_string, '20160318-54ebe070') self.assertEqual(rating_string, '9,45,1290.90,-5184.0,69,95,129,157,71,4303,831,830,33,1761')
def test_parse_names_and_ranks(self): decoder = TenhouDecoder() message = '<un n0="%4e%6f%4e%61%6d%65" n1="%6f%32%6f%32" n2="%73%68%69%6d%6d%6d%6d%6d"' \ ' n3="%e5%b7%9d%e6%b5%b7%e8%80%81" dan="0,7,12,1" ' \ 'rate="1500.00,1421.91,1790.94,1532.23" sx="m,m,m,m"/>' values = decoder.parse_names_and_ranks(message) self.assertEqual(values[0], {'name': 'NoName', 'rank': TenhouDecoder.RANKS[0]}) self.assertEqual(values[1], {'name': 'o2o2', 'rank': TenhouDecoder.RANKS[7]}) self.assertEqual(values[2], {'name': 'shimmmmm', 'rank': TenhouDecoder.RANKS[12]}) self.assertEqual(values[3], {'name': u'川海老', 'rank': TenhouDecoder.RANKS[1]})
def test_parse_initial_round_values(self): decoder = TenhouDecoder() message = '<INIT seed="0,2,3,0,1,126" ten="250,250,250,250" oya="3" ' \ 'hai="30,67,44,21,133,123,87,69,36,34,94,4,128"/>' values = decoder.parse_initial_values(message) self.assertEqual(values['round_number'], 0) self.assertEqual(values['count_of_honba_sticks'], 2) self.assertEqual(values['count_of_riichi_sticks'], 3) self.assertEqual(values['dora_indicator'], 126) self.assertEqual(values['dealer'], 3)
def test_parse_initial_round_values(): decoder = TenhouDecoder() message = ( '<INIT seed="0,2,3,0,1,126" ten="250,250,250,250" oya="3" hai="30,67,44,21,133,123,87,69,36,34,94,4,128"/>' ) values = decoder.parse_initial_values(message) assert values["round_wind_number"] == 0 assert values["count_of_honba_sticks"] == 2 assert values["count_of_riichi_sticks"] == 3 assert values["dora_indicator"] == 126 assert values["dealer"] == 3
def test_parse_initial_round_values(self): decoder = TenhouDecoder() message = '<INIT seed="0,2,3,0,1,126" ten="250,250,250,250" oya="3" ' \ 'hai="30,67,44,21,133,123,87,69,36,34,94,4,128"/>' values = decoder.parse_initial_values(message) self.assertEqual(values['round_wind_number'], 0) self.assertEqual(values['count_of_honba_sticks'], 2) self.assertEqual(values['count_of_riichi_sticks'], 3) self.assertEqual(values['dora_indicator'], 126) self.assertEqual(values['dealer'], 3)
def test_auth_message(self): decoder = TenhouDecoder() message = """<HELO uname="%4E%6F%4E%61%6D%65" auth="20160318-54ebe070" ratingscale="" PF4="9,45,1290.90,-5184.0,69,95,129,157,71,4303,831,830,33,1761"/>""" auth_string, rating_string, _ = decoder.parse_hello_string(message) self.assertEqual(auth_string, '20160318-54ebe070') self.assertEqual( rating_string, '9,45,1290.90,-5184.0,69,95,129,157,71,4303,831,830,33,1761')
def __init__(self, log_id, file_path, logger): self.decoder = TenhouDecoder() self.logger = logger if log_id: log_content = self._download_log_content(log_id) elif file_path: with open(file_path, "r") as f: log_content = f.read() else: raise AssertionError("log id or file path should be specified") self.rounds = self._parse_rounds(log_content)
def test_parse_final_scores_and_uma(self): decoder = TenhouDecoder() message = '<agari ba="0,0" hai="12,13,41,46,51,78,80,84,98,101,105" m="51243" ' \ 'machi="101" ten="30,1000,0" yaku="20,1" dorahai="89" who="2" fromwho="1" ' \ 'sc="225,0,240,-10,378,10,157,0" owari="225,-17.0,230,3.0,388,48.0,157,-34.0" />' values = decoder.parse_final_scores_and_uma(message) self.assertEqual(values['scores'], [225, 230, 388, 157]) self.assertEqual(values['uma'], [-17, 3, 48, -34]) message = '<ryuukyoku ten="30,1000,0" sc="225,0,240,-10,378,10,157,0" ' \ 'owari="225,-17.0,230,3.0,388,48.0,157,-34.0" />' values = decoder.parse_final_scores_and_uma(message) self.assertEqual(values['scores'], [225, 230, 388, 157]) self.assertEqual(values['uma'], [-17, 3, 48, -34])
def test_parse_final_scores_and_uma(): decoder = TenhouDecoder() message = ( '<agari ba="0,0" hai="12,13,41,46,51,78,80,84,98,101,105" m="51243" ' 'machi="101" ten="30,1000,0" yaku="20,1" dorahai="89" who="2" fromwho="1" ' 'sc="225,0,240,-10,378,10,157,0" owari="225,-17.0,230,3.0,388,48.0,157,-34.0" />' ) values = decoder.parse_final_scores_and_uma(message) assert values["scores"] == [225, 230, 388, 157] assert values["uma"] == [-17, 3, 48, -34] message = ( '<ryuukyoku ten="30,1000,0" sc="225,0,240,-10,378,10,157,0" owari="225,-17.0,230,3.0,388,48.0,157,-34.0" />' ) values = decoder.parse_final_scores_and_uma(message) assert values["scores"] == [225, 230, 388, 157] assert values["uma"] == [-17, 3, 48, -34]
def test_reconnection_information(): message = ('<REINIT seed="0,0,1,4,3,59" ten="250,250,250,240" oya="0" ' 'hai="1,2,4,13,17,20,46,47,53,71,76,81,85" ' 'm2="41515" ' 'kawa0="120,28,128,131,18,75,74,27,69,130,64" ' 'kawa1="117,121,123,129,103,72,83,125,62,84" ' 'kawa2="33,114,122,107,31,105,78,9,68,73,38" ' 'kawa3="115,0,126,87,24,25,106,255,70,3,119"/>') decoder = TenhouDecoder() result = decoder.parse_table_state_after_reconnection(message) assert len(result) == 4 assert len(result[0]["discards"]) == 11 # one additional tile from meld assert len(result[1]["discards"]) == 11 assert len(result[2]["discards"]) == 11 assert len(result[3]["discards"]) == 10 assert len(result[2]["melds"]) == 1
def test_reconnection_information(self): message = '<REINIT seed="0,0,1,4,3,59" ten="250,250,250,240" oya="0" ' \ 'hai="1,2,4,13,17,20,46,47,53,71,76,81,85" ' \ 'm2="41515" ' \ 'kawa0="120,28,128,131,18,75,74,27,69,130,64" ' \ 'kawa1="117,121,123,129,103,72,83,125,62,84" ' \ 'kawa2="33,114,122,107,31,105,78,9,68,73,38" ' \ 'kawa3="115,0,126,87,24,25,106,255,70,3,119"/>' decoder = TenhouDecoder() result = decoder.parse_table_state_after_reconnection(message) self.assertEqual(len(result), 4) self.assertEqual(len(result[0]['discards']), 11) # one additional tile from meld self.assertEqual(len(result[1]['discards']), 11) self.assertEqual(len(result[2]['discards']), 11) self.assertEqual(len(result[3]['discards']), 10) self.assertEqual(len(result[2]['melds']), 1)
def test_parse_tile(self): decoder = TenhouDecoder() tile = decoder.parse_tile('<t23/>') self.assertEqual(tile, 23) tile = decoder.parse_tile('<e24/>') self.assertEqual(tile, 24) tile = decoder.parse_tile('<f25/>') self.assertEqual(tile, 25) tile = decoder.parse_tile('<g26/>') self.assertEqual(tile, 26) tile = decoder.parse_tile('<f23 t="4"/>') self.assertEqual(tile, 23)
class TenhouLogReproducer(object): """ The way to debug bot decisions that it made in real tenhou.net games """ def __init__(self, log_url, stop_tag=None): log_id, player_position, needed_round = self._parse_url(log_url) log_content = self._download_log_content(log_id) rounds = self._parse_rounds(log_content) self.player_position = player_position self.round_content = rounds[needed_round] self.stop_tag = stop_tag self.decoder = TenhouDecoder() def reproduce(self, dry_run=False): draw_tags = ['T', 'U', 'V', 'W'] discard_tags = ['D', 'E', 'F', 'G'] player_draw = draw_tags[self.player_position] player_draw_regex = re.compile('^<[{}]+\d*'.format(''.join(player_draw))) discard_regex = re.compile('^<[{}]+\d*'.format(''.join(discard_tags))) table = Table() for tag in self.round_content: if player_draw_regex.match(tag) and 'UN' not in tag: print('Player draw') tile = self.decoder.parse_tile(tag) table.player.draw_tile(tile) if dry_run: if self._is_draw(tag): print('<-', TilesConverter.to_one_line_string([self._parse_tile(tag)]), tag) elif self._is_discard(tag): print('->', TilesConverter.to_one_line_string([self._parse_tile(tag)]), tag) elif self._is_init_tag(tag): hands = { 0: [int(x) for x in self._get_attribute_content(tag, 'hai0').split(',')], 1: [int(x) for x in self._get_attribute_content(tag, 'hai1').split(',')], 2: [int(x) for x in self._get_attribute_content(tag, 'hai2').split(',')], 3: [int(x) for x in self._get_attribute_content(tag, 'hai3').split(',')], } print('Initial hand:', TilesConverter.to_one_line_string(hands[self.player_position])) else: print(tag) if not dry_run and tag == self.stop_tag: break if 'INIT' in tag: values = self.decoder.parse_initial_values(tag) shifted_scores = [] for x in range(0, 4): shifted_scores.append(values['scores'][self._normalize_position(x, self.player_position)]) table.init_round( values['round_wind_number'], values['count_of_honba_sticks'], values['count_of_riichi_sticks'], values['dora_indicator'], self._normalize_position(self.player_position, values['dealer']), shifted_scores, ) hands = [ [int(x) for x in self.decoder.get_attribute_content(tag, 'hai0').split(',')], [int(x) for x in self.decoder.get_attribute_content(tag, 'hai1').split(',')], [int(x) for x in self.decoder.get_attribute_content(tag, 'hai2').split(',')], [int(x) for x in self.decoder.get_attribute_content(tag, 'hai3').split(',')], ] table.player.init_hand(hands[self.player_position]) if discard_regex.match(tag) and 'DORA' not in tag: tile = self.decoder.parse_tile(tag) player_sign = tag.upper()[1] player_seat = self._normalize_position(self.player_position, discard_tags.index(player_sign)) if player_seat == 0: table.player.discard_tile(tile) else: table.add_discarded_tile(player_seat, tile, False) if '<N who=' in tag: meld = self.decoder.parse_meld(tag) player_seat = self._normalize_position(self.player_position, meld.who) table.add_called_meld(player_seat, meld) if player_seat == 0: # we had to delete called tile from hand # to have correct tiles count in the hand if meld.type != Meld.KAN and meld.type != Meld.CHANKAN: table.player.draw_tile(meld.called_tile) if '<REACH' in tag and 'step="1"' in tag: who_called_riichi = self._normalize_position(self.player_position, self.decoder.parse_who_called_riichi(tag)) table.add_called_riichi(who_called_riichi) if not dry_run: tile = self.decoder.parse_tile(self.stop_tag) print('Hand: {}'.format(table.player.format_hand_for_print(tile))) # to rebuild all caches table.player.draw_tile(tile) tile = table.player.discard_tile() # real run, you can stop debugger here table.player.draw_tile(tile) tile = table.player.discard_tile() print('Discard: {}'.format(TilesConverter.to_one_line_string([tile]))) def _normalize_position(self, who, from_who): positions = [0, 1, 2, 3] return positions[who - from_who] def _parse_url(self, log_url): temp = log_url.split('?')[1].split('&') log_id, player, round_wind = '', 0, 0 for item in temp: item = item.split('=') if 'log' == item[0]: log_id = item[1] if 'tw' == item[0]: player = int(item[1]) if 'ts' == item[0]: round_wind = int(item[1]) return log_id, player, round_wind def _download_log_content(self, log_id): """ Check the log file, and if it is not there download it from tenhou.net :param log_id: :return: """ temp_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'logs') if not os.path.exists(temp_folder): os.mkdir(temp_folder) log_file = os.path.join(temp_folder, log_id) if os.path.exists(log_file): with open(log_file, 'r') as f: return f.read() else: url = 'http://e.mjv.jp/0/log/?{0}'.format(log_id) response = requests.get(url) with open(log_file, 'w') as f: f.write(response.text) return response.text def _parse_rounds(self, log_content): """ Build list of round tags :param log_content: :return: """ rounds = [] game_round = [] tag_start = 0 tag = None for x in range(0, len(log_content)): if log_content[x] == '>': tag = log_content[tag_start:x + 1] tag_start = x + 1 # not useful tags if tag and ('mjloggm' in tag or 'TAIKYOKU' in tag): tag = None # new round was started if tag and 'INIT' in tag: rounds.append(game_round) game_round = [] # the end of the game if tag and 'owari' in tag: rounds.append(game_round) if tag: # to save some memory we can remove not needed information from logs if 'INIT' in tag: # we dont need seed information find = re.compile(r'shuffle="[^"]*"') tag = find.sub('', tag) # add processed tag to the round game_round.append(tag) tag = None return rounds[1:] def _is_discard(self, tag): skip_tags = ['<GO', '<FURITEN', '<DORA'] if any([x in tag for x in skip_tags]): return False match_discard = re.match(r"^<[defgDEFG]+\d*", tag) if match_discard: return True return False def _is_draw(self, tag): match_discard = re.match(r"^<[tuvwTUVW]+\d*", tag) if match_discard: return True return False def _parse_tile(self, tag): result = re.match(r'^<[defgtuvwDEFGTUVW]+\d*', tag).group() return int(result[2:]) def _is_init_tag(self, tag): return tag and 'INIT' in tag def _get_attribute_content(self, tag, attribute_name): result = re.findall(r'{}="([^"]*)"'.format(attribute_name), tag) return result and result[0] or None
def test_parse_who_called_riichi(self): decoder = TenhouDecoder() who = decoder.parse_who_called_riichi('<REACH who="2" ten="255,216,261,258" step="2"/>') self.assertEqual(who, 2)