Skip to content
This repository was archived by the owner on Jul 8, 2023. It is now read-only.

Commit 8423640

Browse files
committed
Add round reproducer
1 parent 80000b9 commit 8423640

File tree

4 files changed

+366
-3
lines changed

4 files changed

+366
-3
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ project/settings_local.py
99
old_version.py
1010

1111
tests_validate_hand.py
12-
reproducer.py
1312
loader.py
1413
socket_mock.py
1514
*.db

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,51 @@ They will override settings from default `settings.py` file
8484
2. Also you can override some default settings with command argument.
8585
Use `python main.py -h` to check all available commands
8686

87+
## Round reproducer
88+
89+
We built the way to reproduce already played round.
90+
This is really helpful when you want to reproduce table state and fix bot bad behaviour.
91+
92+
There are two options to do it.
93+
94+
### Reproduce from tenhou log link
95+
96+
First you need to do dry run of the reproducer with command:
97+
98+
```
99+
python reproducer.py -o "http://tenhou.net/0/?log=2017041516gm-0089-0000-23b4752d&tw=3&ts=2" -d
100+
```
101+
102+
It will print all available tags in the round. For example we want to stop before
103+
discard tile to other player ron, in given example we had to chose `<W59/>` tag as a stop tag.
104+
105+
Next command will be:
106+
107+
```
108+
python reproducer.py -o "http://tenhou.net/0/?log=2017041516gm-0089-0000-23b4752d&tw=3&ts=2" -t "<W59/>"
109+
```
110+
111+
And output:
112+
113+
```
114+
Hand: 268m28p23456677s + 6p
115+
Discard: 2m
116+
```
117+
118+
After this you can debug bot decisions.
119+
120+
### Reproduce from our log
121+
122+
Sometimes we had to debug bot <-> server communication. For this purpose we built this reproducer.
123+
124+
Just use it with already played game:
125+
126+
```
127+
python reproducer.py -l d6a5e_2017-04-13\ 09_54_01.log
128+
```
129+
130+
It will send to the bot all commands that were send from tenhou in real game.
131+
87132
## Code checking
88133

89134
This command will check the code style: `flake8 --config=../.flake8`

project/reproducer.py

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import os
2+
import re
3+
from optparse import OptionParser
4+
5+
import logging
6+
import requests
7+
8+
from mahjong.ai.discard import DiscardOption
9+
from mahjong.meld import Meld
10+
from mahjong.table import Table
11+
from mahjong.tile import TilesConverter
12+
from tenhou.client import TenhouClient
13+
from tenhou.decoder import TenhouDecoder
14+
from utils.logger import set_up_logging
15+
16+
17+
logger = logging.getLogger('tenhou')
18+
19+
20+
class TenhouLogReproducer(object):
21+
"""
22+
The way to debug bot decisions that it made in real tenhou.net games
23+
"""
24+
25+
def __init__(self, log_url, stop_tag=None):
26+
log_id, player_position, needed_round = self._parse_url(log_url)
27+
log_content = self._download_log_content(log_id)
28+
rounds = self._parse_rounds(log_content)
29+
30+
self.player_position = player_position
31+
self.round_content = rounds[needed_round]
32+
self.stop_tag = stop_tag
33+
self.decoder = TenhouDecoder()
34+
35+
def reproduce(self, dry_run=False):
36+
draw_tags = ['T', 'U', 'V', 'W']
37+
discard_tags = ['D', 'E', 'F', 'G']
38+
39+
player_draw = draw_tags[self.player_position]
40+
41+
player_draw_regex = re.compile('^<[{}]+\d*'.format(''.join(player_draw)))
42+
discard_regex = re.compile('^<[{}]+\d*'.format(''.join(discard_tags)))
43+
44+
table = Table()
45+
for tag in self.round_content:
46+
if dry_run:
47+
print(tag)
48+
49+
if not dry_run and tag == self.stop_tag:
50+
break
51+
52+
if 'INIT' in tag:
53+
values = self.decoder.parse_initial_values(tag)
54+
55+
shifted_scores = []
56+
for x in range(0, 4):
57+
shifted_scores.append(values['scores'][self._normalize_position(x, self.player_position)])
58+
59+
table.init_round(
60+
values['round_number'],
61+
values['count_of_honba_sticks'],
62+
values['count_of_riichi_sticks'],
63+
values['dora_indicator'],
64+
self._normalize_position(self.player_position, values['dealer']),
65+
shifted_scores,
66+
)
67+
68+
hands = [
69+
[int(x) for x in self.decoder.get_attribute_content(tag, 'hai0').split(',')],
70+
[int(x) for x in self.decoder.get_attribute_content(tag, 'hai1').split(',')],
71+
[int(x) for x in self.decoder.get_attribute_content(tag, 'hai2').split(',')],
72+
[int(x) for x in self.decoder.get_attribute_content(tag, 'hai3').split(',')],
73+
]
74+
75+
table.player.init_hand(hands[self.player_position])
76+
77+
if player_draw_regex.match(tag) and 'UN' not in tag:
78+
tile = self.decoder.parse_tile(tag)
79+
table.player.draw_tile(tile)
80+
81+
if discard_regex.match(tag) and 'DORA' not in tag:
82+
tile = self.decoder.parse_tile(tag)
83+
player_sign = tag.upper()[1]
84+
player_seat = self._normalize_position(self.player_position, discard_tags.index(player_sign))
85+
86+
if player_seat == 0:
87+
table.player.discard_tile(DiscardOption(table.player, tile // 4, 0, [], 0))
88+
else:
89+
table.add_discarded_tile(player_seat, tile, False)
90+
91+
if '<N who=' in tag:
92+
meld = self.decoder.parse_meld(tag)
93+
player_seat = self._normalize_position(self.player_position, meld.who)
94+
table.add_called_meld(player_seat, meld)
95+
96+
if player_seat == 0:
97+
# we had to delete called tile from hand
98+
# to have correct tiles count in the hand
99+
if meld.type != Meld.KAN and meld.type != Meld.CHANKAN:
100+
table.player.draw_tile(meld.called_tile)
101+
102+
if '<REACH' in tag and 'step="1"' in tag:
103+
who_called_riichi = self._normalize_position(self.player_position,
104+
self.decoder.parse_who_called_riichi(tag))
105+
table.add_called_riichi(who_called_riichi)
106+
107+
if not dry_run:
108+
tile = self.decoder.parse_tile(self.stop_tag)
109+
print('Hand: {}'.format(table.player.format_hand_for_print(tile)))
110+
111+
# to rebuild all caches
112+
table.player.draw_tile(tile)
113+
tile = table.player.discard_tile()
114+
115+
# real run, you can stop debugger here
116+
table.player.draw_tile(tile)
117+
tile = table.player.discard_tile()
118+
119+
print('Discard: {}'.format(TilesConverter.to_one_line_string([tile])))
120+
121+
def _normalize_position(self, who, from_who):
122+
positions = [0, 1, 2, 3]
123+
return positions[who - from_who]
124+
125+
def _parse_url(self, log_url):
126+
temp = log_url.split('?')[1].split('&')
127+
log_id, player, round_number = '', 0, 0
128+
for item in temp:
129+
item = item.split('=')
130+
if 'log' == item[0]:
131+
log_id = item[1]
132+
if 'tw' == item[0]:
133+
player = int(item[1])
134+
if 'ts' == item[0]:
135+
round_number = int(item[1])
136+
return log_id, player, round_number
137+
138+
def _download_log_content(self, log_id):
139+
"""
140+
Check the log file, and if it is not there download it from tenhou.net
141+
:param log_id:
142+
:return:
143+
"""
144+
temp_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'logs')
145+
if not os.path.exists(temp_folder):
146+
os.mkdir(temp_folder)
147+
148+
log_file = os.path.join(temp_folder, log_id)
149+
if os.path.exists(log_file):
150+
with open(log_file, 'r') as f:
151+
return f.read()
152+
else:
153+
url = 'http://e.mjv.jp/0/log/?{0}'.format(log_id)
154+
response = requests.get(url)
155+
156+
with open(log_file, 'w') as f:
157+
f.write(response.text)
158+
159+
return response.text
160+
161+
def _parse_rounds(self, log_content):
162+
"""
163+
Build list of round tags
164+
:param log_content:
165+
:return:
166+
"""
167+
rounds = []
168+
169+
game_round = []
170+
tag_start = 0
171+
tag = None
172+
for x in range(0, len(log_content)):
173+
if log_content[x] == '>':
174+
tag = log_content[tag_start:x + 1]
175+
tag_start = x + 1
176+
177+
# not useful tags
178+
if tag and ('mjloggm' in tag or 'TAIKYOKU' in tag):
179+
tag = None
180+
181+
# new round was started
182+
if tag and 'INIT' in tag:
183+
rounds.append(game_round)
184+
game_round = []
185+
186+
# the end of the game
187+
if tag and 'owari' in tag:
188+
rounds.append(game_round)
189+
190+
if tag:
191+
# to save some memory we can remove not needed information from logs
192+
if 'INIT' in tag:
193+
# we dont need seed information
194+
find = re.compile(r'shuffle="[^"]*"')
195+
tag = find.sub('', tag)
196+
197+
# add processed tag to the round
198+
game_round.append(tag)
199+
tag = None
200+
201+
return rounds[1:]
202+
203+
204+
class SocketMock(object):
205+
"""
206+
Reproduce tenhou <-> bot communication
207+
"""
208+
209+
def __init__(self, log_path):
210+
self.log_path = log_path
211+
self.commands = []
212+
self.text = self._load_text()
213+
self._parse_text()
214+
215+
def connect(self, _):
216+
pass
217+
218+
def shutdown(self, _):
219+
pass
220+
221+
def close(self):
222+
pass
223+
224+
def sendall(self, message):
225+
pass
226+
227+
def recv(self, _):
228+
if not self.commands:
229+
raise KeyboardInterrupt('End of commands')
230+
231+
return self.commands.pop(0).encode('utf-8')
232+
233+
def _load_text(self):
234+
log_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), self.log_path)
235+
with open(log_file, 'r') as f:
236+
return f.read()
237+
238+
def _parse_text(self):
239+
"""
240+
Load list of get commands that tenhou.net sent to us
241+
"""
242+
results = self.text.split('\n')
243+
for item in results:
244+
if 'Get: ' not in item:
245+
continue
246+
247+
item = item.split('Get: ')[1]
248+
item = item.replace('> <', '>\x00<')
249+
item += '\x00'
250+
251+
self.commands.append(item)
252+
253+
254+
def parse_args_and_start_reproducer():
255+
parser = OptionParser()
256+
257+
parser.add_option('-o', '--online_log',
258+
type='string',
259+
help='Tenhou log with specified player and round number. '
260+
'Example: http://tenhou.net/0/?log=2017041516gm-0089-0000-23b4752d&tw=3&ts=2')
261+
262+
parser.add_option('-l', '--local_log',
263+
type='string',
264+
help='Path to local log file')
265+
266+
parser.add_option('-d', '--dry_run',
267+
action='store_true',
268+
default=False,
269+
help='Special option for tenhou log reproducer. '
270+
'If true, it will print all available tags in the round')
271+
272+
parser.add_option('-t', '--tag',
273+
type='string',
274+
help='Special option for tenhou log reproducer. It indicates where to stop parse round tags')
275+
276+
opts, _ = parser.parse_args()
277+
278+
if not opts.online_log and not opts.local_log:
279+
print('Please, set -o or -l option')
280+
return
281+
282+
if opts.online_log and not opts.dry_run and not opts.tag:
283+
print('Please, set -t for real run of the online log')
284+
return
285+
286+
if opts.online_log:
287+
if '?' not in opts.online_log and '&' not in opts.online_log:
288+
print('Wrong tenhou log format, please provide log link with player position and round number')
289+
return
290+
291+
reproducer = TenhouLogReproducer(opts.online_log, opts.tag)
292+
reproducer.reproduce(opts.dry_run)
293+
else:
294+
set_up_logging()
295+
296+
client = TenhouClient(SocketMock(opts.local_log))
297+
try:
298+
client.connect()
299+
client.authenticate()
300+
client.start_game()
301+
except (Exception, KeyboardInterrupt) as e:
302+
logger.exception('', exc_info=e)
303+
client.end_game()
304+
305+
306+
def main():
307+
parse_args_and_start_reproducer()
308+
309+
310+
if __name__ == '__main__':
311+
main()

project/tenhou/client.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,21 @@ class TenhouClient(Client):
3131

3232
_count_of_empty_messages = 0
3333
_rating_string = None
34+
_socket_mock = None
3435

35-
def __init__(self):
36+
def __init__(self, socket_mock=None):
3637
super().__init__()
3738
self.statistics = Statistics()
39+
self._socket_mock = socket_mock
3840

3941
def connect(self):
40-
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
42+
# for reproducer
43+
if self._socket_mock:
44+
self.socket = self._socket_mock
45+
TenhouClient.SLEEP_BETWEEN_ACTIONS = 0
46+
else:
47+
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
48+
4149
self.socket.connect((settings.TENHOU_HOST, settings.TENHOU_PORT))
4250

4351
def authenticate(self):

0 commit comments

Comments
 (0)