ffzeroHua commited on
Commit
f825909
·
verified ·
1 Parent(s): fe06770

Update core.py

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