ffzeroHua commited on
Commit
1d05c4e
·
verified ·
1 Parent(s): 20e126c

Update core_for_test_3.py

Browse files
Files changed (1) hide show
  1. core_for_test_3.py +811 -809
core_for_test_3.py CHANGED
@@ -1,809 +1,811 @@
1
- import socket
2
- import ssl
3
- import base64
4
- import hashlib
5
- import os
6
- import time
7
- import json
8
- import threading
9
- import Bot
10
- import socks
11
- import sys
12
- import random
13
- from urllib.parse import unquote
14
- import requests
15
- from datetime import datetime
16
-
17
- #socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 4156)
18
- socket.socket = socks.socksocket
19
-
20
- LEFT_HEARTBEAT = 10
21
- bot = Bot.Bot()
22
-
23
- def CallBot(roomid, bots=['Default'], speed=2, nplayer=3):
24
- return '等待别人加入'
25
- url = "https://review.bigcoach.work/api/v2/dispatch"
26
- headers = {
27
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0",
28
- "Accept": "application/json, text/plain, */*",
29
- "Content-Type": "application/json"
30
- }
31
- data = {
32
- "roomid": roomid,
33
- "bots": bots,
34
- "speed": speed,
35
- "nplayer": nplayer
36
- }
37
- return requests.post(url, json=data, headers=headers, proxies=None).text
38
-
39
- ROOM = '4442'
40
- MODE = '25'
41
- ROOM_ID = ROOM + ',' + MODE
42
-
43
- result_file = None
44
-
45
- host = "b-ww.mjv.jp"
46
- path = "/"
47
-
48
- def WriteToResult(s):
49
- with open('TraineeRecord.txt', 'a') as f:
50
- f.write('*' + str(s))
51
-
52
- class WebSocketClient:
53
- def __init__(self, on_message_callback=None):
54
- self.sock = None
55
- self.running = False
56
- self.on_message = on_message_callback or self.default_on_message
57
- self.receive_thread = None
58
- self.max_idle_cycle = 5
59
- self.idle_cycle = self.max_idle_cycle
60
- self.max_retry = 0
61
- self.last_message = ''
62
- self.last_account = 'NoName'
63
-
64
- def default_on_message(self, msg):
65
- """默认的消息处理函数"""
66
- print(f"Received: {msg}")
67
-
68
- def login(self, account):
69
- self.last_account = account
70
- socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 4156)
71
- socket.socket = socks.socksocket
72
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
73
-
74
- context = ssl.create_default_context()
75
- wrapped_socket = context.wrap_socket(sock, server_hostname=host)
76
- wrapped_socket.connect((host, 443))
77
-
78
- key = base64.b64encode(hashlib.sha1(str(id(wrapped_socket)).encode()).digest()[:16])
79
-
80
- request = (
81
- f"GET {path} HTTP/1.1\r\n"
82
- f"Host: b-ww.mjv.jp\r\n"
83
- f"Upgrade: websocket\r\n"
84
- f"Connection: Upgrade\r\n"
85
- f"Sec-WebSocket-Key: {key.decode()}\r\n"
86
- f"Sec-WebSocket-Version: 13\r\n"
87
- f"Origin: https://tenhou.net\r\n"
88
- f"User-Agent: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0\r\n"
89
- "\r\n"
90
- )
91
- wrapped_socket.send(request.encode())
92
-
93
- response = wrapped_socket.recv(4096).decode()
94
- if "101 Switching Protocols" not in response:
95
- print(response)
96
- raise Exception("Log in failed:", response)
97
-
98
- self.sock = wrapped_socket
99
- print(f'\033[33m尝试登录账号:{account}\033[0m')
100
- self.send('{"tag":"HELO","name":"' + str(account) + '","sx":"F"}')
101
- return wrapped_socket
102
-
103
- def send(self, data):
104
- if not self.sock:
105
- raise Exception("Socket not connected")
106
- print('\033[90m<-', data, '\033[0m')
107
- self.last_message = data
108
- frame = bytearray()
109
- frame.append(0x81)
110
- mask_bit = 0x80
111
- payload_len = len(data)
112
-
113
- if payload_len <= 125:
114
- frame.append(mask_bit | payload_len)
115
- elif payload_len <= 65535:
116
- frame.append(mask_bit | 126)
117
- frame.extend(payload_len.to_bytes(2, byteorder='big'))
118
- else:
119
- frame.append(mask_bit | 127)
120
- frame.extend(payload_len.to_bytes(8, byteorder='big'))
121
-
122
- masking_key = bytearray(os.urandom(4))
123
- frame.extend(masking_key)
124
-
125
- masked_data = bytearray()
126
- for i in range(payload_len):
127
- masked_data.append(ord(data[i]) ^ masking_key[i % 4])
128
- frame.extend(masked_data)
129
-
130
- self.sock.send(frame)
131
-
132
- def _recv_frame(self):
133
- if not self.sock:
134
- return None
135
-
136
- header = self.sock.recv(2)
137
- if not header:
138
- return None
139
-
140
- fin = (header[0] & 0x80) != 0
141
- opcode = header[0] & 0x0F
142
- mask = (header[1] & 0x80) != 0
143
- length = header[1] & 0x7F
144
-
145
- if length == 126:
146
- length = int.from_bytes(self.sock.recv(2), byteorder="big")
147
- elif length == 127:
148
- length = int.from_bytes(self.sock.recv(8), byteorder="big")
149
-
150
- if mask:
151
- masking_key = self.sock.recv(4)
152
- data = self.sock.recv(length)
153
- decoded = bytearray([data[i] ^ masking_key[i % 4] for i in range(length)])
154
- else:
155
- decoded = self.sock.recv(length)
156
-
157
- return decoded.decode()
158
-
159
- def _receive_loop(self):
160
- """接收消息的循环,运行在单独的线程中"""
161
- while self.running:
162
- try:
163
- msg = self._recv_frame()
164
- if msg:
165
- print(f"\033[90m-> {msg}\033[0m")
166
- self.idle_cycle = self.max_idle_cycle
167
- self.on_message(self, msg)
168
- time.sleep(0.1) # 短暂休眠避免CPU占用过高
169
- except EOFError as e:
170
- if self.running: # 只有在运行状态下才打印错误
171
- print(f"Error receiving message: {e}")
172
-
173
- break
174
-
175
- def start(self, account):
176
- """启动WebSocket连接和消息接收"""
177
- self.login(account)
178
- self.running = True
179
- self.receive_thread = threading.Thread(target=self._receive_loop)
180
- self.receive_thread.daemon = True # 设置为守护线程
181
- self.receive_thread.start()
182
-
183
- def stop(self):
184
- """停止WebSocket连接"""
185
- self.running = False
186
- if self.sock:
187
- self.sock.close()
188
- if self.receive_thread:
189
- self.receive_thread.join(timeout=1.0)
190
-
191
- def heartbeat(self):
192
- self.send('<Z/>')
193
- self.idle_cycle -= 1
194
- if self.idle_cycle <= 0:
195
- self.max_retry -= 1
196
- if self.max_retry <= 0:
197
- print('\033[91m连接已断开\033[0m')
198
- os._exit(0)
199
- print('\033[33m服务器无应答,尝试重新连接数据包\033[0m')
200
- WriteToResult('北风(Tenhou):网络波动,似乎掉线')
201
- self.idle_cycle = 1
202
- self.stop()
203
- self.login(self.last_account)
204
-
205
- class State:
206
- def __init__(self):
207
- self.seat = 0
208
- self.reached = False
209
- self.is3p = False
210
- self.hand = []
211
- self.tsumo = False
212
- self.round_name = ''
213
- self.paipu_url = ''
214
- self.last_dahai_player = 0
215
- self.owari = False
216
- self.is_new_round = True
217
-
218
- state = State()
219
- tiles_tenhou: dict[str, int] = {
220
- '1m': 0, '2m': 1, '3m': 2, '4m': 3, '5m': 4, '5mr': 4, '6m': 5, '7m': 6, '8m': 7, '9m': 8,
221
- '1p': 9, '2p': 10, '3p': 11, '4p': 12, '5p': 13, '5pr': 13, '6p': 14, '7p': 15, '8p': 16, '9p': 17,
222
- '1s': 18, '2s': 19, '3s': 20, '4s': 21, '5s': 22, '5sr': 22, '6s': 23, '7s': 24, '8s': 25, '9s': 26,
223
- 'E': 27, 'S': 28, 'W': 29, 'N': 30, 'P': 31, 'F': 32, 'C': 33
224
- }
225
-
226
- def ToTenhou(state, labels: list[str]) -> list[int]:
227
- ret = []
228
- hand = state.hand[:]
229
- hand = sorted(hand, reverse=True)
230
-
231
- for label in labels:
232
- is_red = label[-1] == 'r'
233
- index = tiles_tenhou[label]
234
- index = [i for i in hand if i // 4 == index and (not is_red or i % 4 == 0) and i not in ret][0]
235
- ret.append(index)
236
-
237
- return ret
238
-
239
- def ToTenhouOne(state, label, tsumogiri=False):
240
- if tsumogiri:
241
- return state.hand[-1]
242
- return ToTenhou(state, [label])[0]
243
-
244
- def RelToAbs(rel: int) -> int:
245
- return (rel + state.seat) % 4
246
-
247
- def AbsToRel(abs: int) -> int:
248
- return (abs - state.seat) % 4
249
-
250
- def StartGame(message):
251
- global state
252
- mjai_messages = [{'type': 'start_game', 'id': 0}]
253
- state.seat = (4 - int(message['oya'])) % 4
254
- mjai_messages[0]['id'] = state.seat
255
- state.paipu_url = f'https://tenhou.net/6/?log={message.get("log","")}&tw={state.seat}'
256
- return mjai_messages
257
-
258
- def ParseScoreList(scores):
259
- new_scores = [-1, -1, -1, -1]
260
- for i in range(4):
261
- new_scores[RelToAbs(i)] = scores[i]
262
- return new_scores
263
-
264
- def StartKyoku(message):
265
- global state
266
- bakaze = ['E', 'S', 'W', 'N']
267
- oya = RelToAbs(int(message['oya']))
268
-
269
- seed = [int(s) for s in message['seed'].split(',')]
270
- bakaze = bakaze[seed[0] // 4]
271
- kyoku = seed[0] % 4 + 1
272
- honba = seed[1]
273
- state.round_name = '东南西北'[seed[0] // 4] + ' ' + str(kyoku) + ' 局 ' + str(honba) + ' 本场'
274
- kyotaku = seed[2]
275
- dora_marker = TileName(seed[5])
276
- scores = [int(s)*100 for s in message['ten'].split(',')]
277
- tehais = [['?' for _ in range(13)]] * 4
278
- state.hand = [int(s) for s in message['hai'].split(',')]
279
- tehais[state.seat] = [TileName(x) for x in state.hand]
280
-
281
- if bakaze == 'E' and kyoku == 1 and honba == 0:
282
- if 0 in scores:
283
- state.is3p = True
284
- if True:
285
- new_scores = [-1, -1, -1, -1]
286
- for i in range(4):
287
- new_scores[RelToAbs(i)] = scores[i]
288
- scores = new_scores
289
- original = scores[:]
290
- if False:
291
- print('原始分数', scores)
292
- if seed[0] // 4 == '南' and kyoku == 3:
293
- scores[state.seat] -= 8000
294
- else:
295
- for i in range(len(scores)):
296
- if scores[i] == 0:
297
- continue
298
- scores[i] -= 16000
299
- if i == state.seat:
300
- scores[i] += 96000
301
- start_kyoku_msg = {
302
- 'type': 'start_kyoku',
303
- 'bakaze': bakaze,
304
- 'kyoku': kyoku,
305
- 'honba': honba,
306
- 'kyotaku': kyotaku,
307
- 'oya': oya,
308
- 'dora_marker': dora_marker,
309
- 'scores': scores,
310
- 'tehais': tehais
311
- }
312
-
313
- return [start_kyoku_msg]
314
-
315
- def Tsumo(message):
316
- global state
317
- tag = message['tag']
318
- actor = RelToAbs(ord(tag[0]) - ord('T'))
319
- mjai_messages = [{
320
- 'type': 'tsumo',
321
- 'actor': actor,
322
- 'pai': '?',
323
- }]
324
- state.tsumo = False
325
- if actor == state.seat:
326
- pai = int(tag[1:])
327
- mjai_messages[0]['pai'] = TileName(pai)
328
- state.hand.append(pai)
329
- state.tsumo = True
330
- return mjai_messages
331
-
332
- def Dahai(message):
333
- global state
334
- tag = message['tag']
335
- actor = RelToAbs(ord(str.upper(tag[0])) - ord('D'))
336
- if len(tag) == 1:
337
- idx = state.hand[-1]
338
- else:
339
- idx = int(tag[1:])
340
- pai = TileName(idx)
341
- tsumogiri = tag[0] in 'defg'
342
-
343
- state.tsuomo = False
344
- if actor == state.seat:
345
- state.hand.remove(idx)
346
- state.last_dahai_player = actor
347
- return [{
348
- 'type': 'dahai',
349
- 'actor': actor,
350
- 'pai': pai,
351
- 'tsumogiri': tsumogiri,
352
- }]
353
-
354
- def Nuki(message, just_drew):
355
- global state
356
- actor = RelToAbs(int(message['who']))
357
- m = int(message['m'])
358
- if (m & 0x3F) == 0x20 :
359
- # nukidora
360
- if actor == state.seat:
361
- for i in state.hand:
362
- if i // 4 == 30:
363
- state.hand.remove(i)
364
- break
365
- return [{
366
- 'type': 'nukidora',
367
- 'actor': actor,
368
- 'pai': 'N'
369
- }]
370
-
371
- meld = DecodeNuki(m, just_drew)
372
- if meld['type'] == 'chi':
373
- target = (actor - 1) % 4
374
- elif meld['type'] in ('daiminkan', 'pon'):
375
- target = state.last_dahai_player
376
- else:
377
- target = RelToAbs(meld['target'] % 4)
378
-
379
- mjai_messages = [{
380
- 'type': meld['type'],
381
- 'actor': actor,
382
- 'target': target,
383
- 'pai': meld['tile'],
384
- 'consumed': meld['consumed']
385
- }]
386
-
387
- if meld['type'] in ['kakan', 'ankan']:
388
- del mjai_messages[0]['target']
389
- if meld['type'] == 'ankan':
390
- del mjai_messages[0]['pai']
391
-
392
- if actor == state.seat:
393
- if meld['type'] == 'kakan':
394
- if mjai_messages[0]['pai'] in state.hand:
395
- state.hand.remove(mjai_messages[0]['pai'])
396
- else:
397
- for i in meld['consumed_id']:
398
- state.hand.remove(i)
399
-
400
- return mjai_messages
401
-
402
- def Reach(message):
403
- global state
404
- actor = RelToAbs(int(message['who']))
405
- return [{'type': 'reach', 'actor': actor}]
406
-
407
- def ReachAccepted(message):
408
- global state
409
- actor = int(message['who'])
410
- state.reached = actor == 0
411
-
412
- deltas = [0] * 4
413
- deltas[actor] = -1000
414
- scores = [int(s) * 100 for s in message['ten'].split(',')]
415
-
416
- return [{
417
- 'type': 'reach_accepted',
418
- 'actor': RelToAbs(actor),
419
- 'deltas': ParseScoreList(deltas),
420
- 'scores': ParseScoreList(scores)
421
- }]
422
-
423
- def Dora(message):
424
- global state
425
- return [{'type': 'dora', 'dora_marker': TileName(int(message['hai']))}]
426
-
427
- def DecodeNuki(m, just_drew):
428
- global state
429
- call_dir = m & 3
430
-
431
- if m & 0x4:
432
- call_type = "chi"
433
- tile_info = m >> 10
434
- rs = [(m >> 3) & 3, 4 + ((m >> 5) & 3), 8 + ((m >> 7) & 3)]
435
- elif m & 0x8 or m & 0x10:
436
- call_type = "pon" if m & 0x8 else "kakan"
437
- tile_info = m >> 9
438
- rs = [0, 1, 2, 3]
439
- if call_type == "pon":
440
- rs.remove((m >> 5) & 3)
441
- # call_dir = state.last_dahai_player
442
- else:
443
- call_type = "nukidora" if m & 0x20 else "ankan" if just_drew else "daiminkan"
444
- tile_info = m >> 8
445
- rs = [0, 1, 2, 3]
446
-
447
- num_tiles = 4 if call_type in {"nukidora", "daiminkan", "ankan"} else 3
448
-
449
- ix = tile_info // num_tiles
450
-
451
- if call_type == "chi":
452
- ix = (ix // 7) * 9 + ix % 7
453
-
454
- called_tile_id = ix * 4 + (rs[0] if call_type == "kakan" else rs[tile_info % num_tiles])
455
- called_tiles_id = [(ix * 4) + r for r in rs]
456
- if call_type not in ('ankan', 'kakan'):
457
- called_tiles_id.remove(called_tile_id)
458
- if call_type == 'kakan':
459
- called_tiles_id = called_tiles_id[0:3]
460
-
461
- return {'type': call_type,
462
- 'target': call_dir,
463
- 'tile': TileName(called_tile_id),
464
- 'consumed': [TileName(x) for x in called_tiles_id],
465
- 'consumed_id': called_tiles_id
466
- }
467
-
468
-
469
-
470
- def ParseSC(message):
471
- sc = [int(s) for s in message['sc'].split(',')]
472
- before = sc[0::2]
473
- delta = sc[1::2]
474
- after = [(x + y) * 100 for x, y in zip(before, delta)]
475
- return after
476
-
477
- def ParseScoreDelta(message):
478
- sc = [int(s) for s in message['sc'].split(',')]
479
- before = sc[0::2]
480
- delta = sc[1::2]
481
- after = [x * 100 for x in delta]
482
- return after
483
-
484
- def RotateScore(scores):
485
- global state
486
- res = [0, 0, 0, 0]
487
- for i, x in enumerate(scores):
488
- res[RelToAbs(i)] = x
489
- return res
490
-
491
- #from downloader import single_work
492
- def Owari(message):
493
- global state
494
- scores = ParseSC(message)
495
- rank = sorted(scores, reverse=True).index(scores[0]) + 1
496
- scores = RotateScore(scores)
497
- #WriteToResult('| ' + datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ' | ' + str(rank) + ' | ' + state.paipu_url + ' |\n')
498
- #with open('训练报告.txt', 'a') as f:
499
- #f.write(str(rank) + ',' + state.paipu_url + '\n')
500
- #single_work(state.paipu_url)
501
-
502
- def Agari(message):
503
- global state
504
- scores = ParseSC(message)
505
- deltas = ParseScoreDelta(message)
506
- scores = RotateScore(scores)
507
- deltas = RotateScore(deltas)
508
- tiles = [int(x) for x in message['hai'].split(',')]
509
- machi = int(message['machi'])
510
- if message['who'] != message['fromWho'] and machi in tiles:
511
- tiles.remove(machi)
512
- ura_dora = []
513
- if 'doraHaiUra' in message:
514
- ura_dora = [TileName(int(x)) for x in message['doraHaiUra'].split(',')]
515
- return [
516
- {'type': 'hora', 'actor': RelToAbs(int(message['who'])), 'tiles': [TileName(x) for x in tiles],
517
- 'target': RelToAbs(int(message['fromWho'])), 'scores': scores, 'delta': deltas,
518
- 'ura_dora': ura_dora},
519
- {'type': 'end_kyoku'}
520
- ]
521
-
522
- def Ryukyoku(message):
523
- global state
524
- deltas = ParseScoreDelta(message)
525
- deltas = RotateScore(deltas)
526
- return [
527
- {'type': 'ryukyoku', 'delta': deltas},
528
- {'type': 'end_kyoku'}
529
- ]
530
-
531
- def GetSeat(tag: str) -> int:
532
- draw_map = {'T': 0, 'U': 1, 'V': 2, 'W': 3}
533
- discard_map = {'D': 0, 'E': 1, 'F': 2, 'G': 3}
534
- if tag[0] in draw_map:
535
- return draw_map[tag[0]]
536
- if tag[0] in discard_map:
537
- return discard_map[tag[0]]
538
- return -1
539
-
540
- def TileName(tile_id):
541
- if tile_id == 16:
542
- return '5mr'
543
- if tile_id == 16 + 36:
544
- return '5pr'
545
- if tile_id == 16 + 72:
546
- return '5sr'
547
- if tile_id // 36 >= 3:
548
- return 'ESWNPFC'[tile_id % 36 // 4]
549
- return str(tile_id % 36 // 4 + 1) + 'mpsz????'[tile_id // 36]
550
-
551
- ACTION_NAME = {
552
- 1: 'pon',
553
- 4: 'chi',
554
- 16: 'akari',
555
- 32: 'riichi',
556
- 128: 'nukidora',
557
- }
558
-
559
- # 生成拔北消息
560
- def NukidoraMsg():
561
- return '{"tag":"N","t":10}'
562
-
563
- # 生成打牌消息
564
- def DahaiMsg(hai):
565
- return '{"tag":"D","p":' + str(hai) + '}'
566
-
567
- # 生成跳过鸣牌的消息
568
- def CancelNukiMsg():
569
- return '{"tag":"N"}'
570
-
571
- def build(mjai_msg):
572
- global state
573
- tenhou_msg = {}
574
- if mjai_msg['type'] == 'dahai':
575
- p = ToTenhouOne(state, mjai_msg['pai'], mjai_msg['tsumogiri'])
576
- tenhou_msg = {'tag': 'D', 'p': p}
577
- elif mjai_msg['type'] == 'hora':
578
- if state.tsumo:
579
- tenhou_msg = {'tag': 'N', 'type': 7}
580
- else:
581
- tenhou_msg = {'tag': 'N', 'type': 6}
582
- elif mjai_msg['type'] == 'reach':
583
- tenhou_msg = {'tag': 'REACH'}
584
- elif mjai_msg['type'] == 'ryukyoku':
585
- tenhou_msg = {'tag': 'N', 'type': 9}
586
- elif mjai_msg['type'] == 'ankan':
587
- hai = ToTenhouOne(state, mjai_msg['consumed'][0]) // 4 * 4
588
- tenhou_msg = {'tag': 'N', 'type': 4, 'hai': hai}
589
- elif mjai_msg['type'] == 'kakan':
590
- hai = ToTenhouOne(state, mjai_msg['pai'])
591
- tenhou_msg = {'tag': 'N', 'type': 5, 'hai': hai}
592
- elif mjai_msg['type'] == 'pon':
593
- hai0, hai1 = ToTenhou(state, mjai_msg['consumed'])
594
- tenhou_msg = {'tag': 'N', 'type': 1, 'hai0': hai0, 'hai1': hai1}
595
- elif mjai_msg['type'] == 'daiminkan':
596
- tenhou_msg = {'tag': 'N', 'type': 2}
597
- elif mjai_msg['type'] == 'chi':
598
- hai0, hai1 = ToTenhou(state, mjai_msg['consumed'])
599
- tenhou_msg = {'tag': 'N', 'type': 3, 'hai0': hai0, 'hai1': hai1}
600
- elif mjai_msg['type'] == 'nukidora':
601
- tenhou_msg = {'tag': 'N', 'type': 10}
602
- elif mjai_msg['type'] == 'none':
603
- tenhou_msg = {'tag': 'N'}
604
- else:
605
- return None
606
- return json.dumps(tenhou_msg)
607
-
608
- def Reinit(msg):
609
- global state
610
- kawas = []
611
- extra_kawas = []
612
- N_count = [0, 0, 0, 0]
613
- for s in range(4):
614
- kawa = [TileName(int(i)) for i in msg.get('kawa' + str(s), '').split(',')]
615
- for s in range(4):
616
- melds = [int(i) for i in msg.get('m' + str(s), '').split(',')]
617
- for meld in melds:
618
- meld = DecodeNuki(meld)
619
- if meld['type'] == 'nukidora':
620
- N_count[s] += 1
621
-
622
- MESSAGES = []
623
- import MjaiChecker
624
-
625
- def Tehai():
626
- global state
627
- tehai = sorted(state.hand[0:-1])
628
- result = ''
629
- last = ''
630
- for i, x in enumerate(tehai):
631
- card = MjaiChecker.TileName(TileName(x))
632
- if last != '' and last[-1] != card[-1] and last[-1] in 'mps':
633
- result += last[-1]
634
- result += card[0]
635
- last = card
636
- if last[-1] in 'mps':
637
- result += last[-1]
638
- result += ' '
639
- return result + MjaiChecker.TileName(TileName(state.hand[-1]))
640
-
641
- # 自定义消息处理函数
642
- def handle_message(client, msg):
643
- global MESSAGES, state, AWARE_LIST, SLOWER, ALIVE_TICK, LEFT_HEARTBEAT, ROOM
644
- new_msg = []
645
- msg = json.loads(msg)
646
- tag = msg.get('tag')
647
-
648
- if tag == 'HELO':
649
- client.send('{"tag":"LOBBY","id":"' + str(ROOM) + '"}')
650
- client.send('{"tag":"JOIN","t":"' + str(ROOM) + ',' + str(MODE) + '"}')
651
- print('\033[93m' + '成功登录,正在匹配对局' + '\033[0m')
652
- for i in range(2):
653
- print('尝试呼叫机器人')
654
- print(CallBot(ROOM))
655
-
656
- # 不知道什么用,疑似是确认加入游戏
657
- elif tag == 'REJOIN':
658
- t = msg.get('t')
659
- client.send('{"tag":"JOIN","t":"' + t + '"}')
660
-
661
- # 开局,相当于点击 OK 准备
662
- elif tag == 'TAIKYOKU' or tag == 'SAIKAI':
663
- ALIVE_TICK = 720
664
- LEFT_HEARTBEAT = 600
665
- state.reached = False
666
- MESSAGES = new_msg = StartGame(msg)
667
- print('\033[93m' + '已匹配对局' + '\033[0m')
668
- client.send('{"tag":"GOK"}')
669
- client.send('{"tag":"NEXTREADY"}')
670
-
671
- # 没什么用
672
- elif tag in ('GO', 'BYE', 'FURITEN'):
673
- return
674
-
675
- # 发生错误
676
- elif tag == 'ERR':
677
- code = msg['code']
678
- print('\033[91m' + f'天凤服务器给出错误代码 {code}' + '\033[0m')
679
- if code == '1004':
680
- print('\033[91m' + f'提示:该账号在其他地方已经登录' + '\033[0m')
681
- elif code == '1003':
682
- print('\033[91m' + f'提示:检查 ID 是否正确,或者该账号已被封禁' + '\033[0m')
683
- os._exit(0)
684
-
685
- # 对战信息
686
- elif tag == 'UN':
687
- if 'n0' in msg:
688
- state.self_name = unquote(msg['n0'])
689
- if 'dan' in msg:
690
- state.dan = (['新人'] + [f'{i} 级' for i in range(9, 0, -1)] +
691
- ['初段', '二段', '三段', '四段', '五段',
692
- '六段', '七段', '八段', '九段', '十段', '天凤'])[int(msg['dan'].split(',')[0])]
693
- # 初始化新的一局
694
- elif tag == 'INIT' or tag == 'REINIT':
695
- new_msg = StartKyoku(msg)
696
- MESSAGES = new_msg
697
- state.is_new_round = True
698
- print('\033[90m检测到新的一局,之后可能会有九种九牌流局\033[0m')
699
-
700
- # 和牌或者流局,准备下一局
701
- elif tag in ('AGARI', 'RYUUKYOKU'):
702
- client.send('{"tag":"NEXTREADY"}')
703
- if tag == 'AGARI':
704
- new_msg = Agari(msg)
705
- else:
706
- new_msg = Ryukyoku(msg)
707
- MESSAGES += new_msg
708
- if 'owari' in msg:
709
- Owari(msg)
710
- state.owari = True
711
-
712
- # 一局结束需要另开一个连接
713
- elif tag == 'PROF':
714
- pass
715
-
716
- # 有人立直
717
- elif tag == 'REACH':
718
- if msg['step'] == '1':
719
- new_msg = Reach(msg)
720
- MESSAGES += new_msg
721
- else:
722
- new_msg = ReachAccepted(msg)
723
- MESSAGES += new_msg
724
- pass
725
-
726
- # 新宝牌指示牌
727
- elif tag == 'DORA':
728
- new_msg = Dora(msg)
729
- MESSAGES += new_msg
730
-
731
- # 摸牌
732
- elif tag[0] in 'TUVW':
733
- new_msg = Tsumo(msg)
734
- MESSAGES += new_msg
735
-
736
- # 鸣牌
737
- elif tag[0] == 'N' and 'm' in msg:
738
- if state.is_new_round:
739
- print('\033[90m不再是新的一局,本局不会有九种九牌流局\033[0m')
740
- new_msg = Nuki(msg, MESSAGES[-1]['type'] == 'tsumo')
741
- MESSAGES += new_msg
742
- state.tsumo = False
743
-
744
- # 打牌
745
- elif tag[0] in 'DEFGdefg':
746
- new_msg = Dahai(msg)
747
- MESSAGES += new_msg
748
- state.tsumo = False
749
-
750
- if len(state.hand) % 3 == 2:
751
- print('\033[92m' + Tehai() + ' \033[90m' + str(sorted(state.hand) + [state.hand[-1]]) + '\033[0m')
752
- for msg in new_msg:
753
- print('\033[93m' + MjaiChecker.TranslateSingle(msg, state.seat) + '\033[0m')
754
- try:
755
- # paiju = process_mjai_events(MESSAGES, state.seat)
756
- # paiju['self'] = state.seat
757
- json.dump(MESSAGES, open('TraineePaiju.txt', 'w', encoding='utf-8'))
758
- except Exception as e:
759
- print('牌局保存失败', e)
760
- if len(new_msg):
761
- print('\033[90m发送给模型:' + json.dumps(new_msg, separators=(",", ":")) + '\033[0m')
762
- res = bot.react(json.dumps(new_msg))
763
- if res:
764
- print('\033[90m模型决策:' + res + '\033[0m')
765
- res = build(json.loads(res))
766
- res = json.loads(res)
767
- if res.get('tag') == 'N' and res.get('type') == 9:
768
- print('\033[90m尝试九种九牌流局\033[0m')
769
- if not state.is_new_round:
770
- print('\033[90m流局失败,默认摸切掉一张\033[0m')
771
- res = {'tag': 'D', 'p': state.hand[-1]}
772
- res = json.dumps(res, separators=(',', ':'))
773
- client.send(str(res))
774
- if state.owari:
775
- print('\033[93m对局结束\033[0m')
776
- os._exit(0)
777
- ASID = 'ID215416DE-EJPGCHM8'
778
- def join_game():
779
- global LEFT_HEARTBEAT, ASID, ROOM
780
- try:
781
- client = WebSocketClient(on_message_callback=handle_message)
782
- client.start(ASID)
783
- LEFT_HEARTBEAT = 6
784
- # 主循环 - 可以发送其他消息
785
- try:
786
- while True:
787
- client.heartbeat()
788
- time.sleep(10)
789
- LEFT_HEARTBEAT -= 1
790
- print('存活剩余', LEFT_HEARTBEAT)
791
- if LEFT_HEARTBEAT <= 4:
792
- print('尝试呼叫机器人')
793
- print(CallBot(ROOM))
794
- if LEFT_HEARTBEAT <= 0:
795
- print('存活到期')
796
- sys.exit(0)
797
- except KeyboardInterrupt:
798
- print("Interrupted by user")
799
-
800
- finally:
801
- client.stop()
802
-
803
- if __name__ == "__main__":
804
- args = sys.argv
805
- if len(args) >= 2:
806
- ROOM = args[1]
807
- ROOM_ID = ROOM + ',' + MODE
808
- print('Training ', ROOM, MODE)
809
- join_game()
 
 
 
1
+ import socket
2
+ import ssl
3
+ import base64
4
+ import hashlib
5
+ import os
6
+ import time
7
+ import json
8
+ import threading
9
+ import Bot
10
+ import socks
11
+ import sys
12
+ import random
13
+ from urllib.parse import unquote
14
+ import requests
15
+ from datetime import datetime
16
+
17
+ #socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 4156)
18
+ socket.socket = socks.socksocket
19
+
20
+ LEFT_HEARTBEAT = 10
21
+ bot = Bot.Bot()
22
+
23
+ def CallBot(roomid, bots=['Default'], speed=2, nplayer=3):
24
+ return '等待别人加入'
25
+ url = "https://review.bigcoach.work/api/v2/dispatch"
26
+ headers = {
27
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0",
28
+ "Accept": "application/json, text/plain, */*",
29
+ "Content-Type": "application/json"
30
+ }
31
+ data = {
32
+ "roomid": roomid,
33
+ "bots": bots,
34
+ "speed": speed,
35
+ "nplayer": nplayer
36
+ }
37
+ return requests.post(url, json=data, headers=headers, proxies=None).text
38
+
39
+ ROOM = '4442'
40
+ MODE = '25'
41
+ ROOM_ID = ROOM + ',' + MODE
42
+
43
+ result_file = None
44
+
45
+ host = "b-ww.mjv.jp"
46
+ path = "/"
47
+
48
+ def WriteToResult(s):
49
+ with open('TraineeRecord.txt', 'a') as f:
50
+ f.write('*' + str(s))
51
+
52
+ class WebSocketClient:
53
+ def __init__(self, on_message_callback=None):
54
+ self.sock = None
55
+ self.running = False
56
+ self.on_message = on_message_callback or self.default_on_message
57
+ self.receive_thread = None
58
+ self.max_idle_cycle = 5
59
+ self.idle_cycle = self.max_idle_cycle
60
+ self.max_retry = 0
61
+ self.last_message = ''
62
+ self.last_account = 'NoName'
63
+
64
+ def default_on_message(self, msg):
65
+ """默认的消息处理函数"""
66
+ print(f"Received: {msg}")
67
+
68
+ def login(self, account):
69
+ self.last_account = account
70
+ socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 4156)
71
+ socket.socket = socks.socksocket
72
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
73
+
74
+ context = ssl.create_default_context()
75
+ wrapped_socket = context.wrap_socket(sock, server_hostname=host)
76
+ wrapped_socket.connect((host, 443))
77
+
78
+ key = base64.b64encode(hashlib.sha1(str(id(wrapped_socket)).encode()).digest()[:16])
79
+
80
+ request = (
81
+ f"GET {path} HTTP/1.1\r\n"
82
+ f"Host: b-ww.mjv.jp\r\n"
83
+ f"Upgrade: websocket\r\n"
84
+ f"Connection: Upgrade\r\n"
85
+ f"Sec-WebSocket-Key: {key.decode()}\r\n"
86
+ f"Sec-WebSocket-Version: 13\r\n"
87
+ f"Origin: https://tenhou.net\r\n"
88
+ f"User-Agent: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0\r\n"
89
+ "\r\n"
90
+ )
91
+ wrapped_socket.send(request.encode())
92
+
93
+ response = wrapped_socket.recv(4096).decode()
94
+ if "101 Switching Protocols" not in response:
95
+ print(response)
96
+ raise Exception("Log in failed:", response)
97
+
98
+ self.sock = wrapped_socket
99
+ print(f'\033[33m尝试登录账号:{account}\033[0m')
100
+ self.send('{"tag":"HELO","name":"' + str(account) + '","sx":"F"}')
101
+ return wrapped_socket
102
+
103
+ def send(self, data):
104
+ if not self.sock:
105
+ raise Exception("Socket not connected")
106
+ print('\033[90m<-', data, '\033[0m')
107
+ self.last_message = data
108
+ frame = bytearray()
109
+ frame.append(0x81)
110
+ mask_bit = 0x80
111
+ payload_len = len(data)
112
+
113
+ if payload_len <= 125:
114
+ frame.append(mask_bit | payload_len)
115
+ elif payload_len <= 65535:
116
+ frame.append(mask_bit | 126)
117
+ frame.extend(payload_len.to_bytes(2, byteorder='big'))
118
+ else:
119
+ frame.append(mask_bit | 127)
120
+ frame.extend(payload_len.to_bytes(8, byteorder='big'))
121
+
122
+ masking_key = bytearray(os.urandom(4))
123
+ frame.extend(masking_key)
124
+
125
+ masked_data = bytearray()
126
+ for i in range(payload_len):
127
+ masked_data.append(ord(data[i]) ^ masking_key[i % 4])
128
+ frame.extend(masked_data)
129
+
130
+ self.sock.send(frame)
131
+
132
+ def _recv_frame(self):
133
+ if not self.sock:
134
+ return None
135
+
136
+ header = self.sock.recv(2)
137
+ if not header:
138
+ return None
139
+
140
+ fin = (header[0] & 0x80) != 0
141
+ opcode = header[0] & 0x0F
142
+ mask = (header[1] & 0x80) != 0
143
+ length = header[1] & 0x7F
144
+
145
+ if length == 126:
146
+ length = int.from_bytes(self.sock.recv(2), byteorder="big")
147
+ elif length == 127:
148
+ length = int.from_bytes(self.sock.recv(8), byteorder="big")
149
+
150
+ if mask:
151
+ masking_key = self.sock.recv(4)
152
+ data = self.sock.recv(length)
153
+ decoded = bytearray([data[i] ^ masking_key[i % 4] for i in range(length)])
154
+ else:
155
+ decoded = self.sock.recv(length)
156
+
157
+ return decoded.decode()
158
+
159
+ def _receive_loop(self):
160
+ """接收消息的循环,运行在单独的线程中"""
161
+ while self.running:
162
+ try:
163
+ msg = self._recv_frame()
164
+ if msg:
165
+ print(f"\033[90m-> {msg}\033[0m")
166
+ self.idle_cycle = self.max_idle_cycle
167
+ self.on_message(self, msg)
168
+ time.sleep(0.1) # 短暂休眠避免CPU占用过高
169
+ except EOFError as e:
170
+ if self.running: # 只有在运行状态下才打印错误
171
+ print(f"Error receiving message: {e}")
172
+
173
+ break
174
+
175
+ def start(self, account):
176
+ """启动WebSocket连接和消息接收"""
177
+ self.login(account)
178
+ self.running = True
179
+ self.receive_thread = threading.Thread(target=self._receive_loop)
180
+ self.receive_thread.daemon = True # 设置为守护线程
181
+ self.receive_thread.start()
182
+
183
+ def stop(self):
184
+ """停止WebSocket连接"""
185
+ self.running = False
186
+ if self.sock:
187
+ self.sock.close()
188
+ if self.receive_thread:
189
+ self.receive_thread.join(timeout=1.0)
190
+
191
+ def heartbeat(self):
192
+ self.send('<Z/>')
193
+ self.idle_cycle -= 1
194
+ if self.idle_cycle <= 0:
195
+ self.max_retry -= 1
196
+ if self.max_retry <= 0:
197
+ print('\033[91m连接已断开\033[0m')
198
+ os._exit(0)
199
+ print('\033[33m服务器无应答,尝试重新连接数据包\033[0m')
200
+ WriteToResult('北风(Tenhou):网络波动,似乎掉线')
201
+ self.idle_cycle = 1
202
+ self.stop()
203
+ self.login(self.last_account)
204
+
205
+ class State:
206
+ def __init__(self):
207
+ self.seat = 0
208
+ self.reached = False
209
+ self.is3p = False
210
+ self.hand = []
211
+ self.tsumo = False
212
+ self.round_name = ''
213
+ self.paipu_url = ''
214
+ self.last_dahai_player = 0
215
+ self.owari = False
216
+ self.is_new_round = True
217
+
218
+ state = State()
219
+ tiles_tenhou: dict[str, int] = {
220
+ '1m': 0, '2m': 1, '3m': 2, '4m': 3, '5m': 4, '5mr': 4, '6m': 5, '7m': 6, '8m': 7, '9m': 8,
221
+ '1p': 9, '2p': 10, '3p': 11, '4p': 12, '5p': 13, '5pr': 13, '6p': 14, '7p': 15, '8p': 16, '9p': 17,
222
+ '1s': 18, '2s': 19, '3s': 20, '4s': 21, '5s': 22, '5sr': 22, '6s': 23, '7s': 24, '8s': 25, '9s': 26,
223
+ 'E': 27, 'S': 28, 'W': 29, 'N': 30, 'P': 31, 'F': 32, 'C': 33
224
+ }
225
+
226
+ def ToTenhou(state, labels: list[str]) -> list[int]:
227
+ ret = []
228
+ hand = state.hand[:]
229
+ hand = sorted(hand, reverse=True)
230
+
231
+ for label in labels:
232
+ is_red = label[-1] == 'r'
233
+ index = tiles_tenhou[label]
234
+ index = [i for i in hand if i // 4 == index and (not is_red or i % 4 == 0) and i not in ret][0]
235
+ ret.append(index)
236
+
237
+ return ret
238
+
239
+ def ToTenhouOne(state, label, tsumogiri=False):
240
+ if tsumogiri:
241
+ return state.hand[-1]
242
+ return ToTenhou(state, [label])[0]
243
+
244
+ def RelToAbs(rel: int) -> int:
245
+ return (rel + state.seat) % 4
246
+
247
+ def AbsToRel(abs: int) -> int:
248
+ return (abs - state.seat) % 4
249
+
250
+ def StartGame(message):
251
+ global state
252
+ mjai_messages = [{'type': 'start_game', 'id': 0}]
253
+ state.seat = (4 - int(message['oya'])) % 4
254
+ mjai_messages[0]['id'] = state.seat
255
+ state.paipu_url = f'https://tenhou.net/6/?log={message.get("log","")}&tw={state.seat}'
256
+ return mjai_messages
257
+
258
+ def ParseScoreList(scores):
259
+ new_scores = [-1, -1, -1, -1]
260
+ for i in range(4):
261
+ new_scores[RelToAbs(i)] = scores[i]
262
+ return new_scores
263
+
264
+ def StartKyoku(message):
265
+ global state
266
+ bakaze = ['E', 'S', 'W', 'N']
267
+ oya = RelToAbs(int(message['oya']))
268
+
269
+ seed = [int(s) for s in message['seed'].split(',')]
270
+ bakaze = bakaze[seed[0] // 4]
271
+ kyoku = seed[0] % 4 + 1
272
+ honba = seed[1]
273
+ state.round_name = '东南西北'[seed[0] // 4] + ' ' + str(kyoku) + ' 局 ' + str(honba) + ' 本场'
274
+ kyotaku = seed[2]
275
+ dora_marker = TileName(seed[5])
276
+ scores = [int(s)*100 for s in message['ten'].split(',')]
277
+ tehais = [['?' for _ in range(13)]] * 4
278
+ state.hand = [int(s) for s in message['hai'].split(',')]
279
+ tehais[state.seat] = [TileName(x) for x in state.hand]
280
+
281
+ if bakaze == 'E' and kyoku == 1 and honba == 0:
282
+ if 0 in scores:
283
+ state.is3p = True
284
+ if True:
285
+ new_scores = [-1, -1, -1, -1]
286
+ for i in range(4):
287
+ new_scores[RelToAbs(i)] = scores[i]
288
+ scores = new_scores
289
+ original = scores[:]
290
+ if False:
291
+ print('原始分数', scores)
292
+ if seed[0] // 4 == '南' and kyoku == 3:
293
+ scores[state.seat] -= 8000
294
+ else:
295
+ for i in range(len(scores)):
296
+ if scores[i] == 0:
297
+ continue
298
+ scores[i] -= 16000
299
+ if i == state.seat:
300
+ scores[i] += 96000
301
+ start_kyoku_msg = {
302
+ 'type': 'start_kyoku',
303
+ 'bakaze': bakaze,
304
+ 'kyoku': kyoku,
305
+ 'honba': honba,
306
+ 'kyotaku': kyotaku,
307
+ 'oya': oya,
308
+ 'dora_marker': dora_marker,
309
+ 'scores': scores,
310
+ 'tehais': tehais
311
+ }
312
+
313
+ return [start_kyoku_msg]
314
+
315
+ def Tsumo(message):
316
+ global state
317
+ tag = message['tag']
318
+ actor = RelToAbs(ord(tag[0]) - ord('T'))
319
+ mjai_messages = [{
320
+ 'type': 'tsumo',
321
+ 'actor': actor,
322
+ 'pai': '?',
323
+ }]
324
+ state.tsumo = False
325
+ if actor == state.seat:
326
+ pai = int(tag[1:])
327
+ mjai_messages[0]['pai'] = TileName(pai)
328
+ state.hand.append(pai)
329
+ state.tsumo = True
330
+ return mjai_messages
331
+
332
+ def Dahai(message):
333
+ global state
334
+ tag = message['tag']
335
+ actor = RelToAbs(ord(str.upper(tag[0])) - ord('D'))
336
+ if len(tag) == 1:
337
+ idx = state.hand[-1]
338
+ else:
339
+ idx = int(tag[1:])
340
+ pai = TileName(idx)
341
+ tsumogiri = tag[0] in 'defg'
342
+
343
+ state.tsuomo = False
344
+ if actor == state.seat:
345
+ state.hand.remove(idx)
346
+ state.last_dahai_player = actor
347
+ return [{
348
+ 'type': 'dahai',
349
+ 'actor': actor,
350
+ 'pai': pai,
351
+ 'tsumogiri': tsumogiri,
352
+ }]
353
+
354
+ def Nuki(message, just_drew):
355
+ global state
356
+ actor = RelToAbs(int(message['who']))
357
+ m = int(message['m'])
358
+ if (m & 0x3F) == 0x20 :
359
+ # nukidora
360
+ if actor == state.seat:
361
+ for i in state.hand:
362
+ if i // 4 == 30:
363
+ state.hand.remove(i)
364
+ break
365
+ return [{
366
+ 'type': 'nukidora',
367
+ 'actor': actor,
368
+ 'pai': 'N'
369
+ }]
370
+
371
+ meld = DecodeNuki(m, just_drew)
372
+ if meld['type'] == 'chi':
373
+ target = (actor - 1) % 4
374
+ elif meld['type'] in ('daiminkan', 'pon'):
375
+ target = state.last_dahai_player
376
+ else:
377
+ target = RelToAbs(meld['target'] % 4)
378
+
379
+ mjai_messages = [{
380
+ 'type': meld['type'],
381
+ 'actor': actor,
382
+ 'target': target,
383
+ 'pai': meld['tile'],
384
+ 'consumed': meld['consumed']
385
+ }]
386
+
387
+ if meld['type'] in ['kakan', 'ankan']:
388
+ del mjai_messages[0]['target']
389
+ if meld['type'] == 'ankan':
390
+ del mjai_messages[0]['pai']
391
+
392
+ if actor == state.seat:
393
+ if meld['type'] == 'kakan':
394
+ if mjai_messages[0]['pai'] in state.hand:
395
+ state.hand.remove(mjai_messages[0]['pai'])
396
+ else:
397
+ for i in meld['consumed_id']:
398
+ state.hand.remove(i)
399
+
400
+ return mjai_messages
401
+
402
+ def Reach(message):
403
+ global state
404
+ actor = RelToAbs(int(message['who']))
405
+ return [{'type': 'reach', 'actor': actor}]
406
+
407
+ def ReachAccepted(message):
408
+ global state
409
+ actor = int(message['who'])
410
+ state.reached = actor == 0
411
+
412
+ deltas = [0] * 4
413
+ deltas[actor] = -1000
414
+ scores = [int(s) * 100 for s in message['ten'].split(',')]
415
+
416
+ return [{
417
+ 'type': 'reach_accepted',
418
+ 'actor': RelToAbs(actor),
419
+ 'deltas': ParseScoreList(deltas),
420
+ 'scores': ParseScoreList(scores)
421
+ }]
422
+
423
+ def Dora(message):
424
+ global state
425
+ return [{'type': 'dora', 'dora_marker': TileName(int(message['hai']))}]
426
+
427
+ def DecodeNuki(m, just_drew):
428
+ global state
429
+ call_dir = m & 3
430
+
431
+ if m & 0x4:
432
+ call_type = "chi"
433
+ tile_info = m >> 10
434
+ rs = [(m >> 3) & 3, 4 + ((m >> 5) & 3), 8 + ((m >> 7) & 3)]
435
+ elif m & 0x8 or m & 0x10:
436
+ call_type = "pon" if m & 0x8 else "kakan"
437
+ tile_info = m >> 9
438
+ rs = [0, 1, 2, 3]
439
+ if call_type == "pon":
440
+ rs.remove((m >> 5) & 3)
441
+ # call_dir = state.last_dahai_player
442
+ else:
443
+ call_type = "nukidora" if m & 0x20 else "ankan" if just_drew else "daiminkan"
444
+ tile_info = m >> 8
445
+ rs = [0, 1, 2, 3]
446
+
447
+ num_tiles = 4 if call_type in {"nukidora", "daiminkan", "ankan"} else 3
448
+
449
+ ix = tile_info // num_tiles
450
+
451
+ if call_type == "chi":
452
+ ix = (ix // 7) * 9 + ix % 7
453
+
454
+ called_tile_id = ix * 4 + (rs[0] if call_type == "kakan" else rs[tile_info % num_tiles])
455
+ called_tiles_id = [(ix * 4) + r for r in rs]
456
+ if call_type not in ('ankan', 'kakan'):
457
+ called_tiles_id.remove(called_tile_id)
458
+ if call_type == 'kakan':
459
+ called_tiles_id = called_tiles_id[0:3]
460
+
461
+ return {'type': call_type,
462
+ 'target': call_dir,
463
+ 'tile': TileName(called_tile_id),
464
+ 'consumed': [TileName(x) for x in called_tiles_id],
465
+ 'consumed_id': called_tiles_id
466
+ }
467
+
468
+
469
+
470
+ def ParseSC(message):
471
+ sc = [int(s) for s in message['sc'].split(',')]
472
+ before = sc[0::2]
473
+ delta = sc[1::2]
474
+ after = [(x + y) * 100 for x, y in zip(before, delta)]
475
+ return after
476
+
477
+ def ParseScoreDelta(message):
478
+ sc = [int(s) for s in message['sc'].split(',')]
479
+ before = sc[0::2]
480
+ delta = sc[1::2]
481
+ after = [x * 100 for x in delta]
482
+ return after
483
+
484
+ def RotateScore(scores):
485
+ global state
486
+ res = [0, 0, 0, 0]
487
+ for i, x in enumerate(scores):
488
+ res[RelToAbs(i)] = x
489
+ return res
490
+
491
+ #from downloader import single_work
492
+ def Owari(message):
493
+ global state
494
+ scores = ParseSC(message)
495
+ rank = sorted(scores, reverse=True).index(scores[0]) + 1
496
+ scores = RotateScore(scores)
497
+ #WriteToResult('| ' + datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ' | ' + str(rank) + ' | ' + state.paipu_url + ' |\n')
498
+ #with open('训练报告.txt', 'a') as f:
499
+ #f.write(str(rank) + ',' + state.paipu_url + '\n')
500
+ #single_work(state.paipu_url)
501
+
502
+ def Agari(message):
503
+ global state
504
+ scores = ParseSC(message)
505
+ deltas = ParseScoreDelta(message)
506
+ scores = RotateScore(scores)
507
+ deltas = RotateScore(deltas)
508
+ tiles = [int(x) for x in message['hai'].split(',')]
509
+ machi = int(message['machi'])
510
+ if message['who'] != message['fromWho'] and machi in tiles:
511
+ tiles.remove(machi)
512
+ ura_dora = []
513
+ if 'doraHaiUra' in message:
514
+ ura_dora = [TileName(int(x)) for x in message['doraHaiUra'].split(',')]
515
+ return [
516
+ {'type': 'hora', 'actor': RelToAbs(int(message['who'])), 'tiles': [TileName(x) for x in tiles],
517
+ 'target': RelToAbs(int(message['fromWho'])), 'scores': scores, 'delta': deltas,
518
+ 'ura_dora': ura_dora},
519
+ {'type': 'end_kyoku'}
520
+ ]
521
+
522
+ def Ryukyoku(message):
523
+ global state
524
+ deltas = ParseScoreDelta(message)
525
+ deltas = RotateScore(deltas)
526
+ return [
527
+ {'type': 'ryukyoku', 'delta': deltas},
528
+ {'type': 'end_kyoku'}
529
+ ]
530
+
531
+ def GetSeat(tag: str) -> int:
532
+ draw_map = {'T': 0, 'U': 1, 'V': 2, 'W': 3}
533
+ discard_map = {'D': 0, 'E': 1, 'F': 2, 'G': 3}
534
+ if tag[0] in draw_map:
535
+ return draw_map[tag[0]]
536
+ if tag[0] in discard_map:
537
+ return discard_map[tag[0]]
538
+ return -1
539
+
540
+ def TileName(tile_id):
541
+ if tile_id == 16:
542
+ return '5mr'
543
+ if tile_id == 16 + 36:
544
+ return '5pr'
545
+ if tile_id == 16 + 72:
546
+ return '5sr'
547
+ if tile_id // 36 >= 3:
548
+ return 'ESWNPFC'[tile_id % 36 // 4]
549
+ return str(tile_id % 36 // 4 + 1) + 'mpsz????'[tile_id // 36]
550
+
551
+ ACTION_NAME = {
552
+ 1: 'pon',
553
+ 4: 'chi',
554
+ 16: 'akari',
555
+ 32: 'riichi',
556
+ 128: 'nukidora',
557
+ }
558
+
559
+ # 生成拔北消息
560
+ def NukidoraMsg():
561
+ return '{"tag":"N","t":10}'
562
+
563
+ # 生成打牌消息
564
+ def DahaiMsg(hai):
565
+ return '{"tag":"D","p":' + str(hai) + '}'
566
+
567
+ # 生成跳过鸣牌的消息
568
+ def CancelNukiMsg():
569
+ return '{"tag":"N"}'
570
+
571
+ def build(mjai_msg):
572
+ global state
573
+ tenhou_msg = {}
574
+ if mjai_msg['type'] == 'dahai':
575
+ p = ToTenhouOne(state, mjai_msg['pai'], mjai_msg['tsumogiri'])
576
+ tenhou_msg = {'tag': 'D', 'p': p}
577
+ elif mjai_msg['type'] == 'hora':
578
+ if state.tsumo:
579
+ tenhou_msg = {'tag': 'N', 'type': 7}
580
+ else:
581
+ tenhou_msg = {'tag': 'N', 'type': 6}
582
+ elif mjai_msg['type'] == 'reach':
583
+ tenhou_msg = {'tag': 'REACH'}
584
+ elif mjai_msg['type'] == 'ryukyoku':
585
+ tenhou_msg = {'tag': 'N', 'type': 9}
586
+ elif mjai_msg['type'] == 'ankan':
587
+ hai = ToTenhouOne(state, mjai_msg['consumed'][0]) // 4 * 4
588
+ tenhou_msg = {'tag': 'N', 'type': 4, 'hai': hai}
589
+ elif mjai_msg['type'] == 'kakan':
590
+ hai = ToTenhouOne(state, mjai_msg['pai'])
591
+ tenhou_msg = {'tag': 'N', 'type': 5, 'hai': hai}
592
+ elif mjai_msg['type'] == 'pon':
593
+ hai0, hai1 = ToTenhou(state, mjai_msg['consumed'])
594
+ tenhou_msg = {'tag': 'N', 'type': 1, 'hai0': hai0, 'hai1': hai1}
595
+ elif mjai_msg['type'] == 'daiminkan':
596
+ tenhou_msg = {'tag': 'N', 'type': 2}
597
+ elif mjai_msg['type'] == 'chi':
598
+ hai0, hai1 = ToTenhou(state, mjai_msg['consumed'])
599
+ tenhou_msg = {'tag': 'N', 'type': 3, 'hai0': hai0, 'hai1': hai1}
600
+ elif mjai_msg['type'] == 'nukidora':
601
+ tenhou_msg = {'tag': 'N', 'type': 10}
602
+ elif mjai_msg['type'] == 'none':
603
+ tenhou_msg = {'tag': 'N'}
604
+ else:
605
+ return None
606
+ return json.dumps(tenhou_msg)
607
+
608
+ def Reinit(msg):
609
+ global state
610
+ kawas = []
611
+ extra_kawas = []
612
+ N_count = [0, 0, 0, 0]
613
+ for s in range(4):
614
+ kawa = [TileName(int(i)) for i in msg.get('kawa' + str(s), '').split(',')]
615
+ for s in range(4):
616
+ melds = [int(i) for i in msg.get('m' + str(s), '').split(',')]
617
+ for meld in melds:
618
+ meld = DecodeNuki(meld)
619
+ if meld['type'] == 'nukidora':
620
+ N_count[s] += 1
621
+
622
+ MESSAGES = []
623
+ import MjaiChecker
624
+
625
+ def Tehai():
626
+ global state
627
+ tehai = sorted(state.hand[0:-1])
628
+ result = ''
629
+ last = ''
630
+ for i, x in enumerate(tehai):
631
+ card = MjaiChecker.TileName(TileName(x))
632
+ if last != '' and last[-1] != card[-1] and last[-1] in 'mps':
633
+ result += last[-1]
634
+ result += card[0]
635
+ last = card
636
+ if last[-1] in 'mps':
637
+ result += last[-1]
638
+ result += ' '
639
+ return result + MjaiChecker.TileName(TileName(state.hand[-1]))
640
+
641
+ # 自定义消息处理函数
642
+ def handle_message(client, msg):
643
+ global MESSAGES, state, AWARE_LIST, SLOWER, ALIVE_TICK, LEFT_HEARTBEAT, ROOM
644
+ new_msg = []
645
+ msg = json.loads(msg)
646
+ tag = msg.get('tag')
647
+
648
+ if tag == 'HELO':
649
+ client.send('{"tag":"LOBBY","id":"' + str(ROOM) + '"}')
650
+ client.send('{"tag":"JOIN","t":"' + str(ROOM) + ',' + str(MODE) + '"}')
651
+ print('\033[93m' + '成功登录,正在匹配对局' + '\033[0m')
652
+ for i in range(2):
653
+ print('尝试呼叫机器人')
654
+ print(CallBot(ROOM))
655
+
656
+ # 不知道什么用,疑似是确认加入游戏
657
+ elif tag == 'REJOIN':
658
+ t = msg.get('t')
659
+ client.send('{"tag":"JOIN","t":"' + t + '"}')
660
+
661
+ # 开局,相当于点击 OK 准备
662
+ elif tag == 'TAIKYOKU' or tag == 'SAIKAI':
663
+ ALIVE_TICK = 720
664
+ LEFT_HEARTBEAT = 600
665
+ state.reached = False
666
+ MESSAGES = new_msg = StartGame(msg)
667
+ print('\033[93m' + '已匹配对局' + '\033[0m')
668
+ client.send('{"tag":"GOK"}')
669
+ client.send('{"tag":"NEXTREADY"}')
670
+
671
+ # 没什么用
672
+ elif tag in ('GO', 'BYE', 'FURITEN'):
673
+ return
674
+
675
+ # 发生错误
676
+ elif tag == 'ERR':
677
+ code = msg['code']
678
+ print('\033[91m' + f'天凤服务器给出错误代码 {code}' + '\033[0m')
679
+ if code == '1004':
680
+ print('\033[91m' + f'提示:该账号在其他地方已经登录' + '\033[0m')
681
+ elif code == '1003':
682
+ print('\033[91m' + f'提示:检查 ID 是否正确,或者该账号已被封禁' + '\033[0m')
683
+ os._exit(0)
684
+
685
+ # 对战信息
686
+ elif tag == 'UN':
687
+ if 'n0' in msg:
688
+ state.self_name = unquote(msg['n0'])
689
+ if 'dan' in msg:
690
+ state.dan = (['新人'] + [f'{i} 级' for i in range(9, 0, -1)] +
691
+ ['初段', '二段', '三段', '四段', '五段',
692
+ '六段', '七段', '八段', '九段', '十段', '天凤'])[int(msg['dan'].split(',')[0])]
693
+ # 初始化新的一局
694
+ elif tag == 'INIT' or tag == 'REINIT':
695
+ new_msg = StartKyoku(msg)
696
+ MESSAGES = new_msg
697
+ state.is_new_round = True
698
+ print('\033[90m检测到新的一局,之后可能会有九种九牌流局\033[0m')
699
+
700
+ # 和牌或者流局,准备下一局
701
+ elif tag in ('AGARI', 'RYUUKYOKU'):
702
+ client.send('{"tag":"NEXTREADY"}')
703
+ if tag == 'AGARI':
704
+ new_msg = Agari(msg)
705
+ else:
706
+ new_msg = Ryukyoku(msg)
707
+ MESSAGES += new_msg
708
+ if 'owari' in msg:
709
+ Owari(msg)
710
+ state.owari = True
711
+
712
+ # 一局结束需要另开一个连接
713
+ elif tag == 'PROF':
714
+ pass
715
+
716
+ # 有人立直
717
+ elif tag == 'REACH':
718
+ if msg['step'] == '1':
719
+ new_msg = Reach(msg)
720
+ MESSAGES += new_msg
721
+ else:
722
+ new_msg = ReachAccepted(msg)
723
+ MESSAGES += new_msg
724
+ pass
725
+
726
+ # 新宝牌指示牌
727
+ elif tag == 'DORA':
728
+ new_msg = Dora(msg)
729
+ MESSAGES += new_msg
730
+
731
+ # 摸牌
732
+ elif tag[0] in 'TUVW':
733
+ new_msg = Tsumo(msg)
734
+ MESSAGES += new_msg
735
+
736
+ # 鸣牌
737
+ elif tag[0] == 'N' and 'm' in msg:
738
+ if state.is_new_round:
739
+ print('\033[90m不再是新的一局,本局不会有九种九牌流局\033[0m')
740
+ new_msg = Nuki(msg, MESSAGES[-1]['type'] == 'tsumo')
741
+ MESSAGES += new_msg
742
+ state.tsumo = False
743
+
744
+ # 打牌
745
+ elif tag[0] in 'DEFGdefg':
746
+ new_msg = Dahai(msg)
747
+ MESSAGES += new_msg
748
+ state.tsumo = False
749
+
750
+ if len(state.hand) % 3 == 2:
751
+ print('\033[92m' + Tehai() + ' \033[90m' + str(sorted(state.hand) + [state.hand[-1]]) + '\033[0m')
752
+ for msg in new_msg:
753
+ print('\033[93m' + MjaiChecker.TranslateSingle(msg, state.seat) + '\033[0m')
754
+ try:
755
+ # paiju = process_mjai_events(MESSAGES, state.seat)
756
+ # paiju['self'] = state.seat
757
+ json.dump(MESSAGES, open('TraineePaiju.txt', 'w', encoding='utf-8'))
758
+ except Exception as e:
759
+ print('牌局保存失败', e)
760
+ if len(new_msg):
761
+ print('\033[90m发送给模型:' + json.dumps(new_msg, separators=(",", ":")) + '\033[0m')
762
+ res = bot.react(json.dumps(new_msg))
763
+ if res:
764
+ print('\033[90m模型决策:' + res + '\033[0m')
765
+ res = build(json.loads(res))
766
+ res = json.loads(res)
767
+ if res.get('tag') == 'N' and res.get('type') == 9:
768
+ print('\033[90m尝试九种九牌流局\033[0m')
769
+ if not state.is_new_round:
770
+ print('\033[90m流局失败,默认摸切掉一张\033[0m')
771
+ res = {'tag': 'D', 'p': state.hand[-1]}
772
+ res = json.dumps(res, separators=(',', ':'))
773
+ client.send(str(res))
774
+ if state.owari:
775
+ print('\033[93m对局结束\033[0m')
776
+ os._exit(0)
777
+ ASID = 'ID215416DE-EJPGCHM8'
778
+ def join_game():
779
+ global LEFT_HEARTBEAT, ASID, ROOM
780
+ try:
781
+ client = WebSocketClient(on_message_callback=handle_message)
782
+ client.start(ASID)
783
+ LEFT_HEARTBEAT = 6
784
+ # 主循环 - 可以发送其他消息
785
+ try:
786
+ while True:
787
+ client.heartbeat()
788
+ time.sleep(10)
789
+ LEFT_HEARTBEAT -= 1
790
+ print('存活剩余', LEFT_HEARTBEAT)
791
+ if LEFT_HEARTBEAT <= 4:
792
+ print('尝试呼叫机器人')
793
+ print(CallBot(ROOM))
794
+ if LEFT_HEARTBEAT <= 0:
795
+ print('存活到期')
796
+ sys.exit(0)
797
+ except KeyboardInterrupt:
798
+ print("Interrupted by user")
799
+
800
+ finally:
801
+ client.stop()
802
+
803
+ if __name__ == "__main__":
804
+ args = sys.argv
805
+ if len(args) >= 2:
806
+ ROOM = args[1]
807
+ if len(args) >= 3:
808
+ ASID = args[2]
809
+ ROOM_ID = ROOM + ',' + MODE
810
+ print(f'Training Room: {ROOM}, Mode: {MODE}, Bot ID: {ASID}')
811
+ join_game()