From a344924a16f7c25cc864e4a70eb1506778a39f75 Mon Sep 17 00:00:00 2001 From: Lator <65785354+latorc@users.noreply.github.com> Date: Thu, 2 May 2024 11:26:41 +0800 Subject: [PATCH 01/21] Update readme.md --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 590164c..027d776 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,8 @@ # 麻将 Copilot / Mahjong Copilot 麻将 AI 助手,基于 mjai (Mortal模型) 实现的机器人。会对游戏对局的每一步进行指导。现支持雀魂三人、四人麻将。 -QQ群:834105526 加入QQ群 +QQ群:834105526 加入QQ群 +Join Discord Mahjong AI Assistant for Majsoul, based on mjai (Mortal model) bot impelementaion. When you are in a Majsoul game, AI will give you step-by-step guidance. Now supports Majsoul 3-person and 4-person game modes. From c48dd7f6f008fce39642272d7a5dab5b739426f9 Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Mon, 27 May 2024 03:48:04 +0800 Subject: [PATCH 02/21] =?UTF-8?q?=E5=8A=A0=E8=BD=BD=20AkagiOT2=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E7=9A=84=E5=BB=BA=E8=AE=AE=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/bot.py | 10 +++++++-- common/lan_str.py | 8 +++++++ common/settings.py | 13 ++++++++++++ gui/settings_window.py | 38 +++++++++++++++++++++++++++++++--- mjai/bot_3p/Put_model.pth_here | 1 + readme.md | 3 +++ 6 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 mjai/bot_3p/Put_model.pth_here diff --git a/bot/bot.py b/bot/bot.py index 9a922a4..cb88759 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -7,6 +7,7 @@ from common.log_helper import LOGGER from common.mj_helper import meta_to_options, MjaiType from common.utils import GameMode, BotNotSupportingMode +from common.settings import Settings def reaction_convert_meta(reaction:dict, is_3p:bool=False): @@ -105,8 +106,13 @@ def _init_bot_impl(self, mode:GameMode=GameMode.MJ4P): import riichi as libriichi self.mjai_bot = libriichi.mjai.Bot(engine, self.seat) elif mode == GameMode.MJ3P: - import libriichi3p - self.mjai_bot = libriichi3p.mjai.Bot(engine, self.seat) + settings = Settings() + if settings.enable_ot2_for_3p: + import riichi3p + self.mjai_bot = riichi3p.online.Bot(self.seat) + else: + import libriichi3p + self.mjai_bot = libriichi3p.mjai.Bot(engine, self.seat) else: raise BotNotSupportingMode(mode) diff --git a/common/lan_str.py b/common/lan_str.py index 565f627..698d2c2 100644 --- a/common/lan_str.py +++ b/common/lan_str.py @@ -50,6 +50,10 @@ class LanStr: AI_MODEL_FILE_3P = "Local Model File (3P)" AKAGI_OT_URL = "AkagiOT Server URL" AKAGI_OT_APIKEY = "AkagiOT API Key" + AKAGI_OT2 = "AKAGI OT2" + ENABLE_AKAGI_OT2_FOR_3P = "Enable AkagiOT2 for 3P(Only Effective when model type is Local)" + AKAGI_OT2_URL = "AkagiOT2 Server URL" + AKAGI_OT2_APIKEY = "AkagiOT2 API Key" MJAPI_URL = "MJAPI Server URL" MJAPI_USER = "MJAPI User" MJAPI_USAGE = "API Usage" @@ -183,6 +187,10 @@ class LanStrZHS(LanStr): AI_MODEL_FILE_3P = "本地模型文件(三麻)" AKAGI_OT_URL = "AkagiOT 服务器地址" AKAGI_OT_APIKEY = "AkagiOT API Key" + AKAGI_OT2 = "AKAGI OT2" + ENABLE_AKAGI_OT2_FOR_3P = "三麻中启用 AkagiOT2(仅在选择 Local 模型时生效)" + AKAGI_OT2_URL = "AkagiOT2 服务器地址" + AKAGI_OT2_APIKEY = "AkagiOT2 API Key" MJAPI_URL = "MJAPI 服务器地址" MJAPI_USER = "MJAPI 用户名" MJAPI_USAGE = "API 用量" diff --git a/common/settings.py b/common/settings.py index 397a277..770b82c 100644 --- a/common/settings.py +++ b/common/settings.py @@ -7,6 +7,7 @@ from . import utils DEFAULT_SETTING_FILE = 'settings.json' +DEFAULT_OT2_JSON = 'settings_ot2.json' class Settings: """ Settings class to load and save settings to json file""" @@ -40,6 +41,10 @@ def __init__(self, json_file:str=DEFAULT_SETTING_FILE) -> None: # akagi ot model self.akagi_ot_url:str = self._get_value("akagi_ot_url", "") self.akagi_ot_apikey:str = self._get_value("akagi_ot_apikey", "") + # akagi ot2 3p model + self.enable_ot2_for_3p:bool = self._get_value("enable_ot2_for_3p", False, self.valid_bool) + self.akagi_ot2_url:str = self._get_value("akagi_ot2_url", "") + self.akagi_ot2_apikey: str = self._get_value("akagi_ot2_apikey", "") # for mjapi self.mjapi_url:str = self._get_value("mjapi_url", "https://mjai.7xcnnw11phu.eu.org", self.valid_url) self.mjapi_user:str = self._get_value("mjapi_user", "") @@ -86,6 +91,14 @@ def save_json(self): if not key.startswith('_') and not callable(value)} with open(self._json_file, 'w', encoding='utf-8') as file: json.dump(settings_to_save, file, indent=4, separators=(', ', ': ')) + + # Save ot2 related settings to settings_ot2.json + ot2_settings = { + "ip": self.akagi_ot2_url, + "key": self.akagi_ot2_apikey + } + with open(DEFAULT_OT2_JSON, 'w', encoding='utf-8') as ot2_file: + json.dump(ot2_settings, ot2_file, indent=4, separators=(', ', ': ')) def _get_value(self, key:str, default_value:any, validator:Callable[[any],bool]=None) -> any: """ Get value from settings dictionary, or return default_value if error""" diff --git a/gui/settings_window.py b/gui/settings_window.py index d63a81a..e891399 100644 --- a/gui/settings_window.py +++ b/gui/settings_window.py @@ -16,8 +16,8 @@ def __init__(self, parent:tk.Frame, setting:Settings): super().__init__(parent) self.st = setting - self.geometry('700x675') - self.minsize(700,675) + self.geometry('800x675') + self.minsize(800,800) # self.resizable(False, False) # set position: within main window parent_x = parent.winfo_x() @@ -151,7 +151,30 @@ def create_widgets(self): _label.grid(row=cur_row, column=0, **args_label) self.akagiot_apikey_var = tk.StringVar(value=self.st.akagi_ot_apikey) string_entry = ttk.Entry(main_frame, textvariable=self.akagiot_apikey_var, width=std_wid*4) - string_entry.grid(row=cur_row, column=1,columnspan=3, **args_entry) + string_entry.grid(row=cur_row, column=1,columnspan=3, **args_entry) + + # Enable Akagi OT2 + cur_row += 1 + _label = ttk.Label(main_frame, text=self.st.lan().AKAGI_OT2) + _label.grid(row=cur_row, column=0, **args_label) + self.enable_ot2_for_3p_var = tk.BooleanVar(value=self.st.enable_ot2_for_3p) + enable_akagi_ot2_entry = ttk.Checkbutton( + main_frame, variable=self.enable_ot2_for_3p_var, text=self.st.lan().ENABLE_AKAGI_OT2_FOR_3P, width=std_wid*3) + enable_akagi_ot2_entry.grid(row=cur_row, column=1, **args_entry) + # Akagi OT2 url + cur_row += 1 + _label = ttk.Label(main_frame, text=self.st.lan().AKAGI_OT2_URL) + _label.grid(row=cur_row, column=0, **args_label) + self.akagiot2_url_var = tk.StringVar(value=self.st.akagi_ot2_url) + string_entry = ttk.Entry(main_frame, textvariable=self.akagiot2_url_var, width=std_wid*4) + string_entry.grid(row=cur_row, column=1,columnspan=3, **args_entry) + # Akagi OT2 API Key + cur_row += 1 + _label = ttk.Label(main_frame, text=self.st.lan().AKAGI_OT2_APIKEY) + _label.grid(row=cur_row, column=0, **args_label) + self.akagiot2_apikey_var = tk.StringVar(value=self.st.akagi_ot2_apikey) + string_entry = ttk.Entry(main_frame, textvariable=self.akagiot2_apikey_var, width=std_wid*4) + string_entry.grid(row=cur_row, column=1,columnspan=3, **args_entry) # MJAPI url cur_row += 1 @@ -292,6 +315,9 @@ def _on_save(self): mode_file_3p_new = self.model_file_3p_var.get() akagi_url_new = self.akagiot_url_var.get() akagi_apikey_new = self.akagiot_apikey_var.get() + enable_ot2_for_3p_new = self.enable_ot2_for_3p_var.get() + akagi_url2_new = self.akagiot2_url_var.get() + akagi_apikey2_new = self.akagiot2_apikey_var.get() mjapi_url_new = self.mjapi_url_var.get() mjapi_user_new = self.mjapi_user_var.get() mjapi_secret_new = self.mjapi_secret_var.get() @@ -302,6 +328,9 @@ def _on_save(self): self.st.model_file_3p != mode_file_3p_new or self.st.akagi_ot_url != akagi_url_new or self.st.akagi_ot_apikey != akagi_apikey_new or + self.st.enable_ot2_for_3p != enable_ot2_for_3p_new or + self.st.akagi_ot2_url != akagi_url2_new or + self.st.akagi_ot2_apikey != akagi_apikey2_new or self.st.mjapi_url != mjapi_url_new or self.st.mjapi_user != mjapi_user_new or self.st.mjapi_secret != mjapi_secret_new or @@ -337,6 +366,9 @@ def _on_save(self): self.st.model_file_3p = mode_file_3p_new self.st.akagi_ot_url = akagi_url_new self.st.akagi_ot_apikey = akagi_apikey_new + self.st.enable_ot2_for_3p = enable_ot2_for_3p_new + self.st.akagi_ot2_url = akagi_url2_new + self.st.akagi_ot2_apikey = akagi_apikey2_new self.st.mjapi_url = mjapi_url_new self.st.mjapi_user = mjapi_user_new self.st.mjapi_secret = mjapi_secret_new diff --git a/mjai/bot_3p/Put_model.pth_here b/mjai/bot_3p/Put_model.pth_here new file mode 100644 index 0000000..c65a16a --- /dev/null +++ b/mjai/bot_3p/Put_model.pth_here @@ -0,0 +1 @@ +Get model.pth from the official Akagi discord channel. \ No newline at end of file diff --git a/readme.md b/readme.md index 027d776..9157101 100644 --- a/readme.md +++ b/readme.md @@ -60,8 +60,11 @@ python main.py ``` ### 配置模型 本程序支持几种模型来源。其中,本地模型(Local)是基于 Akagi 兼容的 Mortal 模型。要获取 Akagi 的模型,请参见 Akagi Github 的说明。 +如需使用 AkagiOT2 模型,请前往 Akagi 的官方 Discord 频道获取对应的 model.pth 和 riichi3p 包并安装。 ### Model Configuration This program supports different types of AI models. The 'Local' Model type uses Mortal models compatible with Akagi. To acquire Akagi's models, please refer to Akagi Github . +To use the AkagiOT2 model, please visit Akagi's official Discord channel to obtain the corresponding model.pth and riichi3p package, and then install them. + ## 截图 / Screenshots From 743d9e46d2a0dd66c712c01559ee167e60cef85b Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Mon, 27 May 2024 03:54:08 +0800 Subject: [PATCH 03/21] Fix typo in setting_windows --- gui/settings_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/settings_window.py b/gui/settings_window.py index e891399..af64ba5 100644 --- a/gui/settings_window.py +++ b/gui/settings_window.py @@ -16,7 +16,7 @@ def __init__(self, parent:tk.Frame, setting:Settings): super().__init__(parent) self.st = setting - self.geometry('800x675') + self.geometry('800x800') self.minsize(800,800) # self.resizable(False, False) # set position: within main window From e28579405a6767e3ee34c227ff160daf5925c317 Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Mon, 27 May 2024 10:59:57 +0800 Subject: [PATCH 04/21] Revert "Update readme.md" This reverts commit a344924a16f7c25cc864e4a70eb1506778a39f75. --- readme.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 9157101..6d92c06 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,7 @@ # 麻将 Copilot / Mahjong Copilot 麻将 AI 助手,基于 mjai (Mortal模型) 实现的机器人。会对游戏对局的每一步进行指导。现支持雀魂三人、四人麻将。 -QQ群:834105526 加入QQ群 -Join Discord +QQ群:834105526 加入QQ群 Mahjong AI Assistant for Majsoul, based on mjai (Mortal model) bot impelementaion. When you are in a Majsoul game, AI will give you step-by-step guidance. Now supports Majsoul 3-person and 4-person game modes. From 51be7b8fc343cbf0e065e9f89af72e2b32dc31fb Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Mon, 27 May 2024 14:20:19 +0800 Subject: [PATCH 05/21] =?UTF-8?q?=E9=85=8D=E7=BD=AE=20OT2=20=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E6=97=B6=E6=A3=80=E6=B5=8B=E5=8F=AF=E7=94=A8=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/factory.py | 17 ++++++++++++----- bot/local/bot_local.py | 27 ++++++++++++++++++++------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/bot/factory.py b/bot/factory.py index 47d8b2f..46856b4 100644 --- a/bot/factory.py +++ b/bot/factory.py @@ -8,17 +8,24 @@ MODEL_TYPE_STRINGS = ["Local", "AkagiOT", "MJAPI"] +OT2_MODEL_PATH = "./mjai/bot_3p/model.pth" def get_bot(settings:Settings) -> Bot: """ create the Bot instance based on settings""" match settings.model_type: - case "Local": - model_files:dict = { - GameMode.MJ4P: sub_file(Folder.MODEL, settings.model_file), - GameMode.MJ3P: sub_file(Folder.MODEL, settings.model_file_3p) - } + case "Local": + if settings.enable_ot2_for_3p: + model_files:dict = { + GameMode.MJ4P: sub_file(Folder.MODEL, settings.model_file), + GameMode.MJ3P: OT2_MODEL_PATH + } + else: + model_files:dict = { + GameMode.MJ4P: sub_file(Folder.MODEL, settings.model_file), + GameMode.MJ3P: sub_file(Folder.MODEL, settings.model_file_3p) + } bot = BotMortalLocal(model_files) case "AkagiOT": bot = BotAkagiOt(settings.akagi_ot_url, settings.akagi_ot_apikey) diff --git a/bot/local/bot_local.py b/bot/local/bot_local.py index d522b54..820aeaa 100644 --- a/bot/local/bot_local.py +++ b/bot/local/bot_local.py @@ -7,6 +7,7 @@ from common.log_helper import LOGGER from bot.local.engine import get_engine from bot.bot import BotMjai, GameMode +from common.settings import Settings class BotMortalLocal(BotMjai): @@ -30,13 +31,25 @@ def __init__(self, model_files:dict[GameMode, str]) -> None: except Exception as e: LOGGER.warning("Cannot create engine for mode %s: %s", k, e, exc_info=True) elif k == GameMode.MJ3P: - # test import libraries for 3p - try: - import libriichi3p - from bot.local.engine3p import get_engine as get_engine_3p - self._engines[k] = get_engine_3p(self.model_files[k]) - except Exception as e: # pylint: disable=broad-except - LOGGER.warning("Cannot create engine for mode %s: %s", k, e, exc_info=True) + settings = Settings() + if settings.enable_ot2_for_3p: + import riichi3p + try : + # 尝试创建一个mjai.bot实例 + riichi3p.online.Bot(1) + self._engines[k] = "./mjai/bot_3p/model.pth" + except Exception as e: + LOGGER.warning("Cannot create bot for OT2 model %s: %s", k, e, exc_info=True) + LOGGER.warning("Could be missing model.pth file in path ./mjai/bot_3p") + pass + else: + # test import libraries for 3p + try: + import libriichi3p + from bot.local.engine3p import get_engine as get_engine_3p + self._engines[k] = get_engine_3p(self.model_files[k]) + except Exception as e: # pylint: disable=broad-except + LOGGER.warning("Cannot create engine for mode %s: %s", k, e, exc_info=True) self._supported_modes = list(self._engines.keys()) if not self._supported_modes: raise LocalModelException("No valid model files found") From 91f535b988eafb6d8b6039c1ec78a39714792f4a Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Mon, 27 May 2024 15:04:52 +0800 Subject: [PATCH 06/21] =?UTF-8?q?=E7=94=A8=E5=88=86=E7=A6=BB=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=E8=A7=A3=E5=86=B3=20rust=20panick=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E5=B4=A9=E6=BA=83=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/local/bot_local.py | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/bot/local/bot_local.py b/bot/local/bot_local.py index 820aeaa..6a92bb3 100644 --- a/bot/local/bot_local.py +++ b/bot/local/bot_local.py @@ -8,6 +8,39 @@ from bot.local.engine import get_engine from bot.bot import BotMjai, GameMode from common.settings import Settings +import multiprocessing + +# 尝试获取mjai.bot实例,该方法可能会导致 panick,需要在分离进程中使用 +def create_bot_instance(queue): + import riichi3p + try: + # 尝试创建一个mjai.bot实例 + riichi3p.online.Bot(1) + queue.put(True) # 将成功的标志放入队列 + except Exception as e: + LOGGER.warning("Cannot create bot: %s", e, exc_info=True) + LOGGER.warning("Could be missing model.pth file in path ./mjai/bot_3p") + queue.put(False) # 将失败的标志放入队列 + +# 使用分离进程尝试创建bot实例 +def try_create_ot2_bot(): + queue = multiprocessing.Queue() + process = multiprocessing.Process(target=create_bot_instance, args=(queue,)) + process.start() + + # 尝试从队列中获取结果,设置超时时间防止无限等待 + try: + success = queue.get(timeout=10) # 设置适当的超时时间,例如10秒 + except Exception as e: + LOGGER.error("Failed to retrieve the result from the subprocess: %s", e) + success = False + + process.join() + + if not success or process.exitcode != 0: + LOGGER.error("Failed to create bot or detected a crash in the subprocess with exit code %s", process.exitcode) + return False + return True class BotMortalLocal(BotMjai): @@ -35,9 +68,12 @@ def __init__(self, model_files:dict[GameMode, str]) -> None: if settings.enable_ot2_for_3p: import riichi3p try : - # 尝试创建一个mjai.bot实例 - riichi3p.online.Bot(1) - self._engines[k] = "./mjai/bot_3p/model.pth" + # 用分离进程尝试创建一个mjai.bot实例 + if try_create_ot2_bot(): + self._engines[k] = "./mjai/bot_3p/model.pth" + else: + LOGGER.warning("Cannot create bot for OT2 model %s.", k, exc_info=True) + LOGGER.warning("Could be missing model.pth file in path ./mjai/bot_3p") except Exception as e: LOGGER.warning("Cannot create bot for OT2 model %s: %s", k, e, exc_info=True) LOGGER.warning("Could be missing model.pth file in path ./mjai/bot_3p") From ae11582db5aae79187a9aaee2478ee468721666c Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Mon, 27 May 2024 15:06:07 +0800 Subject: [PATCH 07/21] =?UTF-8?q?=E9=99=8D=E4=BD=8E=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E5=88=B0=203=20=E7=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/local/bot_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/local/bot_local.py b/bot/local/bot_local.py index 6a92bb3..cd29631 100644 --- a/bot/local/bot_local.py +++ b/bot/local/bot_local.py @@ -30,7 +30,7 @@ def try_create_ot2_bot(): # 尝试从队列中获取结果,设置超时时间防止无限等待 try: - success = queue.get(timeout=10) # 设置适当的超时时间,例如10秒 + success = queue.get(timeout=3) # 设置适当的超时时间,例如10秒 except Exception as e: LOGGER.error("Failed to retrieve the result from the subprocess: %s", e) success = False From 1a869187256134d0fb7ef7d8e765f55c45b23200 Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Mon, 27 May 2024 15:30:03 +0800 Subject: [PATCH 08/21] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=A4=9A=E4=BD=99?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/local/bot_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/local/bot_local.py b/bot/local/bot_local.py index cd29631..430c367 100644 --- a/bot/local/bot_local.py +++ b/bot/local/bot_local.py @@ -30,7 +30,7 @@ def try_create_ot2_bot(): # 尝试从队列中获取结果,设置超时时间防止无限等待 try: - success = queue.get(timeout=3) # 设置适当的超时时间,例如10秒 + success = queue.get(timeout=3) except Exception as e: LOGGER.error("Failed to retrieve the result from the subprocess: %s", e) success = False From b37235d39b8646aba15acfd49008b605ddecb140 Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Tue, 28 May 2024 16:05:27 +0800 Subject: [PATCH 09/21] =?UTF-8?q?=E6=B7=BB=E5=8A=A0BotAkagiOt2=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/akagiot2/bot_akagiot2.py | 82 ++++++++++++++++++++++++++++++++++++ bot/factory.py | 5 ++- 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 bot/akagiot2/bot_akagiot2.py diff --git a/bot/akagiot2/bot_akagiot2.py b/bot/akagiot2/bot_akagiot2.py new file mode 100644 index 0000000..dfeccfb --- /dev/null +++ b/bot/akagiot2/bot_akagiot2.py @@ -0,0 +1,82 @@ +import multiprocessing + +from bot.bot import BotMjai, GameMode +from common.log_helper import LOGGER +from pathlib import Path + +from common.utils import BotNotSupportingMode, Ot2BotCreationError + +model_file_path = "mjai/bot_3p/model.pth" + + +class BotAkagiOt2(BotMjai): + """ Bot implementation for Akagi OT2 model """ + + def __init__(self) -> None: + super().__init__("Akagi OT2 Bot") + self._supported_modes: list[GameMode] = [] + self._check() + + def _check(self): + # check model file + if not Path(model_file_path).exists() or not Path(model_file_path).is_file(): + LOGGER.warning("Cannot find model file for Akagi OT2 model:%s", model_file_path) + if try_create_ot2_bot(): + self._supported_modes.append(GameMode.MJ3P) + else: + LOGGER.warning("Cannot create bot for OT2 model.", exc_info=True) + LOGGER.warning("Could be missing file: %s", model_file_path) + raise Ot2BotCreationError("Failed to create bot instance for Akagi OT2 model.") + pass + + @property + def supported_modes(self) -> list[GameMode]: + """ return supported game modes""" + return self._supported_modes + + # 覆写父类方法 + def _init_bot_impl(self, mode: GameMode = GameMode.MJ3P): + if mode == GameMode.MJ3P: + try: + import riichi3p + self.mjai_bot = riichi3p.online.Bot(self.seat) + except Exception as e: + LOGGER.warning("Cannot create bot for Akagi OT2 model: %s", e, exc_info=True) + LOGGER.warning("Could be missing model.pth file in path mjai/bot_3p") + raise Ot2BotCreationError("Failed to create bot instance for Akagi OT2 model.") + else: + raise BotNotSupportingMode(mode) + + +# 尝试获取mjai.bot实例,该方法可能会导致 panick,需要在分离进程中使用 +def create_bot_instance(queue): + import riichi3p + try: + # 尝试创建一个mjai.bot实例 + riichi3p.online.Bot(1) + queue.put(True) # 将成功的标志放入队列 + except Exception as e: + LOGGER.warning("Cannot create bot: %s", e, exc_info=True) + LOGGER.warning("Could be missing model.pth file in path ./mjai/bot_3p") + queue.put(False) # 将失败的标志放入队列 + + +# 使用分离进程尝试创建bot实例 +def try_create_ot2_bot(): + queue = multiprocessing.Queue() + process = multiprocessing.Process(target=create_bot_instance, args=(queue,)) + process.start() + + # 尝试从队列中获取结果,设置超时时间防止无限等待 + try: + success = queue.get(timeout=3) + except Exception as e: + LOGGER.error("Failed to retrieve the result from the subprocess: %s", e) + success = False + + process.join() + + if not success or process.exitcode != 0: + LOGGER.error("Failed to create bot or detected a crash in the subprocess with exit code %s", process.exitcode) + return False + return True diff --git a/bot/factory.py b/bot/factory.py index 46856b4..5c89e0c 100644 --- a/bot/factory.py +++ b/bot/factory.py @@ -5,9 +5,10 @@ from .local.bot_local import BotMortalLocal from .mjapi.bot_mjapi import BotMjapi from .akagiot.bot_akagiot import BotAkagiOt +from .akagiot2.bot_akagiot2 import BotAkagiOt2 -MODEL_TYPE_STRINGS = ["Local", "AkagiOT", "MJAPI"] +MODEL_TYPE_STRINGS = ["Local", "AkagiOT", "MJAPI", "AkagiOT2"] OT2_MODEL_PATH = "./mjai/bot_3p/model.pth" @@ -29,6 +30,8 @@ def get_bot(settings:Settings) -> Bot: bot = BotMortalLocal(model_files) case "AkagiOT": bot = BotAkagiOt(settings.akagi_ot_url, settings.akagi_ot_apikey) + case "AkagiOT2": + bot = BotAkagiOt2() case "MJAPI": bot = BotMjapi(settings) case _: From 829e53c52d79a7fc7d8c51ec3537544ff283032c Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Tue, 28 May 2024 16:05:56 +0800 Subject: [PATCH 10/21] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20OT2=20=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E5=BC=82=E5=B8=B8=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/utils.py b/common/utils.py index a01fdef..f180b53 100644 --- a/common/utils.py +++ b/common/utils.py @@ -64,10 +64,13 @@ class UiState(Enum): GAME_ENDING = 20 -# === Exceptions === +# === Exceptions === class LocalModelException(Exception): """ Exception for model file error""" +class Ot2BotCreationError(Exception): + """ Exception for OT2 bot creation error""" + class MITMException(Exception): """ Exception for MITM error""" @@ -84,6 +87,8 @@ def error_to_str(error:Exception, lan:LanStr) -> str: """ Convert error to language specific string""" if isinstance(error, LocalModelException): return lan.LOCAL_MODEL_ERROR + elif isinstance(error, Ot2BotCreationError): + return lan.OT2_MODEL_ERROR elif isinstance(error, MitmCertNotInstalled): return lan.MITM_CERT_NOT_INSTALLED + f"{error.args}" elif isinstance(error, MITMException): From dbf62423134df226222cf9df7ee51bf3d056f812 Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Tue, 28 May 2024 16:21:43 +0800 Subject: [PATCH 11/21] =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=A1=B5=E5=8E=BB?= =?UTF-8?q?=E9=99=A4=20OT2=20enable=20=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/lan_str.py | 4 ---- common/settings.py | 3 +-- gui/settings_window.py | 11 ----------- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/common/lan_str.py b/common/lan_str.py index 698d2c2..c4f1f2c 100644 --- a/common/lan_str.py +++ b/common/lan_str.py @@ -50,8 +50,6 @@ class LanStr: AI_MODEL_FILE_3P = "Local Model File (3P)" AKAGI_OT_URL = "AkagiOT Server URL" AKAGI_OT_APIKEY = "AkagiOT API Key" - AKAGI_OT2 = "AKAGI OT2" - ENABLE_AKAGI_OT2_FOR_3P = "Enable AkagiOT2 for 3P(Only Effective when model type is Local)" AKAGI_OT2_URL = "AkagiOT2 Server URL" AKAGI_OT2_APIKEY = "AkagiOT2 API Key" MJAPI_URL = "MJAPI Server URL" @@ -187,8 +185,6 @@ class LanStrZHS(LanStr): AI_MODEL_FILE_3P = "本地模型文件(三麻)" AKAGI_OT_URL = "AkagiOT 服务器地址" AKAGI_OT_APIKEY = "AkagiOT API Key" - AKAGI_OT2 = "AKAGI OT2" - ENABLE_AKAGI_OT2_FOR_3P = "三麻中启用 AkagiOT2(仅在选择 Local 模型时生效)" AKAGI_OT2_URL = "AkagiOT2 服务器地址" AKAGI_OT2_APIKEY = "AkagiOT2 API Key" MJAPI_URL = "MJAPI 服务器地址" diff --git a/common/settings.py b/common/settings.py index 770b82c..25644d8 100644 --- a/common/settings.py +++ b/common/settings.py @@ -34,7 +34,7 @@ def __init__(self, json_file:str=DEFAULT_SETTING_FILE) -> None: # AI Model settings self.model_type:str = self._get_value("model_type", "Local") - """ model type: local, mjapi""" + """ model type: local, mjapi, AkagiOT, AkagiOT2""" # for local model self.model_file:str = self._get_value("model_file", "mortal.pth") self.model_file_3p:str = self._get_value("model_file_3p", "mortal_3p.pth") @@ -42,7 +42,6 @@ def __init__(self, json_file:str=DEFAULT_SETTING_FILE) -> None: self.akagi_ot_url:str = self._get_value("akagi_ot_url", "") self.akagi_ot_apikey:str = self._get_value("akagi_ot_apikey", "") # akagi ot2 3p model - self.enable_ot2_for_3p:bool = self._get_value("enable_ot2_for_3p", False, self.valid_bool) self.akagi_ot2_url:str = self._get_value("akagi_ot2_url", "") self.akagi_ot2_apikey: str = self._get_value("akagi_ot2_apikey", "") # for mjapi diff --git a/gui/settings_window.py b/gui/settings_window.py index af64ba5..190cdc4 100644 --- a/gui/settings_window.py +++ b/gui/settings_window.py @@ -153,14 +153,6 @@ def create_widgets(self): string_entry = ttk.Entry(main_frame, textvariable=self.akagiot_apikey_var, width=std_wid*4) string_entry.grid(row=cur_row, column=1,columnspan=3, **args_entry) - # Enable Akagi OT2 - cur_row += 1 - _label = ttk.Label(main_frame, text=self.st.lan().AKAGI_OT2) - _label.grid(row=cur_row, column=0, **args_label) - self.enable_ot2_for_3p_var = tk.BooleanVar(value=self.st.enable_ot2_for_3p) - enable_akagi_ot2_entry = ttk.Checkbutton( - main_frame, variable=self.enable_ot2_for_3p_var, text=self.st.lan().ENABLE_AKAGI_OT2_FOR_3P, width=std_wid*3) - enable_akagi_ot2_entry.grid(row=cur_row, column=1, **args_entry) # Akagi OT2 url cur_row += 1 _label = ttk.Label(main_frame, text=self.st.lan().AKAGI_OT2_URL) @@ -315,7 +307,6 @@ def _on_save(self): mode_file_3p_new = self.model_file_3p_var.get() akagi_url_new = self.akagiot_url_var.get() akagi_apikey_new = self.akagiot_apikey_var.get() - enable_ot2_for_3p_new = self.enable_ot2_for_3p_var.get() akagi_url2_new = self.akagiot2_url_var.get() akagi_apikey2_new = self.akagiot2_apikey_var.get() mjapi_url_new = self.mjapi_url_var.get() @@ -328,7 +319,6 @@ def _on_save(self): self.st.model_file_3p != mode_file_3p_new or self.st.akagi_ot_url != akagi_url_new or self.st.akagi_ot_apikey != akagi_apikey_new or - self.st.enable_ot2_for_3p != enable_ot2_for_3p_new or self.st.akagi_ot2_url != akagi_url2_new or self.st.akagi_ot2_apikey != akagi_apikey2_new or self.st.mjapi_url != mjapi_url_new or @@ -366,7 +356,6 @@ def _on_save(self): self.st.model_file_3p = mode_file_3p_new self.st.akagi_ot_url = akagi_url_new self.st.akagi_ot_apikey = akagi_apikey_new - self.st.enable_ot2_for_3p = enable_ot2_for_3p_new self.st.akagi_ot2_url = akagi_url2_new self.st.akagi_ot2_apikey = akagi_apikey2_new self.st.mjapi_url = mjapi_url_new From 604cbc18cafb95b8b6c700760161167850d0ecb8 Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Tue, 28 May 2024 16:22:01 +0800 Subject: [PATCH 12/21] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20OT2=20=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/lan_str.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/lan_str.py b/common/lan_str.py index c4f1f2c..c12a4fd 100644 --- a/common/lan_str.py +++ b/common/lan_str.py @@ -91,6 +91,7 @@ class LanStr: GAME_NOT_RUNNING = "Not Launched" # errors LOCAL_MODEL_ERROR = "Local Model Loading Error!" + OT2_MODEL_ERROR = "OT2 Model Loading Error!" MITM_SERVER_ERROR = "MITM Service Error!" MITM_CERT_NOT_INSTALLED = "Run as admin or manually install MITM cert." MAIN_THREAD_ERROR = "Main Thread Error!" @@ -228,6 +229,7 @@ class LanStrZHS(LanStr): GAME_NOT_RUNNING = "未启动" #error LOCAL_MODEL_ERROR = "本地模型加载错误!" + OT2_MODEL_ERROR = "OT2 模型加载错误!" MITM_CERT_NOT_INSTALLED = "以管理员运行或手动安装 MITM 证书" MITM_SERVER_ERROR = "MITM 服务错误!" MAIN_THREAD_ERROR = "主进程发生错误!" From 70ad864f32ac7cab8e25c7f43ec418989bcc6126 Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Tue, 28 May 2024 16:28:41 +0800 Subject: [PATCH 13/21] =?UTF-8?q?Revert=20"=E5=88=A0=E9=99=A4=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E6=B3=A8=E9=87=8A"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 1a869187256134d0fb7ef7d8e765f55c45b23200. --- bot/local/bot_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/local/bot_local.py b/bot/local/bot_local.py index 430c367..cd29631 100644 --- a/bot/local/bot_local.py +++ b/bot/local/bot_local.py @@ -30,7 +30,7 @@ def try_create_ot2_bot(): # 尝试从队列中获取结果,设置超时时间防止无限等待 try: - success = queue.get(timeout=3) + success = queue.get(timeout=3) # 设置适当的超时时间,例如10秒 except Exception as e: LOGGER.error("Failed to retrieve the result from the subprocess: %s", e) success = False From 51e000fdff45cc76160ddbc32273d9a115b95309 Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Tue, 28 May 2024 16:28:54 +0800 Subject: [PATCH 14/21] =?UTF-8?q?Revert=20"=E9=99=8D=E4=BD=8E=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E6=97=B6=E9=97=B4=E5=88=B0=203=20=E7=A7=92"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit ae11582db5aae79187a9aaee2478ee468721666c. --- bot/local/bot_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/local/bot_local.py b/bot/local/bot_local.py index cd29631..6a92bb3 100644 --- a/bot/local/bot_local.py +++ b/bot/local/bot_local.py @@ -30,7 +30,7 @@ def try_create_ot2_bot(): # 尝试从队列中获取结果,设置超时时间防止无限等待 try: - success = queue.get(timeout=3) # 设置适当的超时时间,例如10秒 + success = queue.get(timeout=10) # 设置适当的超时时间,例如10秒 except Exception as e: LOGGER.error("Failed to retrieve the result from the subprocess: %s", e) success = False From c00392f4bfa9f64d2d036d49fe55124af93070cd Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Tue, 28 May 2024 16:29:05 +0800 Subject: [PATCH 15/21] =?UTF-8?q?Revert=20"=E7=94=A8=E5=88=86=E7=A6=BB?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E8=A7=A3=E5=86=B3=20rust=20panick=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E5=B4=A9=E6=BA=83=E7=9A=84=E9=97=AE=E9=A2=98"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 91f535b988eafb6d8b6039c1ec78a39714792f4a. --- bot/local/bot_local.py | 42 +++--------------------------------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/bot/local/bot_local.py b/bot/local/bot_local.py index 6a92bb3..820aeaa 100644 --- a/bot/local/bot_local.py +++ b/bot/local/bot_local.py @@ -8,39 +8,6 @@ from bot.local.engine import get_engine from bot.bot import BotMjai, GameMode from common.settings import Settings -import multiprocessing - -# 尝试获取mjai.bot实例,该方法可能会导致 panick,需要在分离进程中使用 -def create_bot_instance(queue): - import riichi3p - try: - # 尝试创建一个mjai.bot实例 - riichi3p.online.Bot(1) - queue.put(True) # 将成功的标志放入队列 - except Exception as e: - LOGGER.warning("Cannot create bot: %s", e, exc_info=True) - LOGGER.warning("Could be missing model.pth file in path ./mjai/bot_3p") - queue.put(False) # 将失败的标志放入队列 - -# 使用分离进程尝试创建bot实例 -def try_create_ot2_bot(): - queue = multiprocessing.Queue() - process = multiprocessing.Process(target=create_bot_instance, args=(queue,)) - process.start() - - # 尝试从队列中获取结果,设置超时时间防止无限等待 - try: - success = queue.get(timeout=10) # 设置适当的超时时间,例如10秒 - except Exception as e: - LOGGER.error("Failed to retrieve the result from the subprocess: %s", e) - success = False - - process.join() - - if not success or process.exitcode != 0: - LOGGER.error("Failed to create bot or detected a crash in the subprocess with exit code %s", process.exitcode) - return False - return True class BotMortalLocal(BotMjai): @@ -68,12 +35,9 @@ def __init__(self, model_files:dict[GameMode, str]) -> None: if settings.enable_ot2_for_3p: import riichi3p try : - # 用分离进程尝试创建一个mjai.bot实例 - if try_create_ot2_bot(): - self._engines[k] = "./mjai/bot_3p/model.pth" - else: - LOGGER.warning("Cannot create bot for OT2 model %s.", k, exc_info=True) - LOGGER.warning("Could be missing model.pth file in path ./mjai/bot_3p") + # 尝试创建一个mjai.bot实例 + riichi3p.online.Bot(1) + self._engines[k] = "./mjai/bot_3p/model.pth" except Exception as e: LOGGER.warning("Cannot create bot for OT2 model %s: %s", k, e, exc_info=True) LOGGER.warning("Could be missing model.pth file in path ./mjai/bot_3p") From b2ca49bb1bbbdd4392c5a206703940ed87eb155d Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Tue, 28 May 2024 16:31:20 +0800 Subject: [PATCH 16/21] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E5=86=B2=E7=AA=81?= =?UTF-8?q?=EF=BC=8C=E5=9B=9E=E9=80=80=20localbot=E7=9A=84=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/factory.py | 16 +++++----------- bot/local/bot_local.py | 27 +++++++-------------------- 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/bot/factory.py b/bot/factory.py index 5c89e0c..024d356 100644 --- a/bot/factory.py +++ b/bot/factory.py @@ -16,17 +16,11 @@ def get_bot(settings:Settings) -> Bot: """ create the Bot instance based on settings""" match settings.model_type: - case "Local": - if settings.enable_ot2_for_3p: - model_files:dict = { - GameMode.MJ4P: sub_file(Folder.MODEL, settings.model_file), - GameMode.MJ3P: OT2_MODEL_PATH - } - else: - model_files:dict = { - GameMode.MJ4P: sub_file(Folder.MODEL, settings.model_file), - GameMode.MJ3P: sub_file(Folder.MODEL, settings.model_file_3p) - } + case "Local": + model_files:dict = { + GameMode.MJ4P: sub_file(Folder.MODEL, settings.model_file), + GameMode.MJ3P: sub_file(Folder.MODEL, settings.model_file_3p) + } bot = BotMortalLocal(model_files) case "AkagiOT": bot = BotAkagiOt(settings.akagi_ot_url, settings.akagi_ot_apikey) diff --git a/bot/local/bot_local.py b/bot/local/bot_local.py index 820aeaa..d522b54 100644 --- a/bot/local/bot_local.py +++ b/bot/local/bot_local.py @@ -7,7 +7,6 @@ from common.log_helper import LOGGER from bot.local.engine import get_engine from bot.bot import BotMjai, GameMode -from common.settings import Settings class BotMortalLocal(BotMjai): @@ -31,25 +30,13 @@ def __init__(self, model_files:dict[GameMode, str]) -> None: except Exception as e: LOGGER.warning("Cannot create engine for mode %s: %s", k, e, exc_info=True) elif k == GameMode.MJ3P: - settings = Settings() - if settings.enable_ot2_for_3p: - import riichi3p - try : - # 尝试创建一个mjai.bot实例 - riichi3p.online.Bot(1) - self._engines[k] = "./mjai/bot_3p/model.pth" - except Exception as e: - LOGGER.warning("Cannot create bot for OT2 model %s: %s", k, e, exc_info=True) - LOGGER.warning("Could be missing model.pth file in path ./mjai/bot_3p") - pass - else: - # test import libraries for 3p - try: - import libriichi3p - from bot.local.engine3p import get_engine as get_engine_3p - self._engines[k] = get_engine_3p(self.model_files[k]) - except Exception as e: # pylint: disable=broad-except - LOGGER.warning("Cannot create engine for mode %s: %s", k, e, exc_info=True) + # test import libraries for 3p + try: + import libriichi3p + from bot.local.engine3p import get_engine as get_engine_3p + self._engines[k] = get_engine_3p(self.model_files[k]) + except Exception as e: # pylint: disable=broad-except + LOGGER.warning("Cannot create engine for mode %s: %s", k, e, exc_info=True) self._supported_modes = list(self._engines.keys()) if not self._supported_modes: raise LocalModelException("No valid model files found") From b20c6a0272e69d6637d8307dcb0eba5b82d32a33 Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Tue, 28 May 2024 17:02:03 +0800 Subject: [PATCH 17/21] =?UTF-8?q?=E5=BF=BD=E7=95=A5=20model.pth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d51c984..5eaea24 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ MahjongCopilot.spec libriichi3p/*.pyd libriichi3p/*.so chrome_ext/*/ +mjai/bot_3p/model.pth From e6d4afdc6e491b42a19807603d9a97b69a0c34d3 Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Tue, 28 May 2024 17:16:46 +0800 Subject: [PATCH 18/21] =?UTF-8?q?=E8=BF=98=E5=8E=9F=20bot.py=20=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/bot.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 96615d8..f4e8d17 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -7,7 +7,6 @@ from common.log_helper import LOGGER from common.mj_helper import meta_to_options, MjaiType from common.utils import GameMode, BotNotSupportingMode -from common.settings import Settings def reaction_convert_meta(reaction:dict, is_3p:bool=False): @@ -110,13 +109,8 @@ def _init_bot_impl(self, mode:GameMode=GameMode.MJ4P): import riichi as libriichi self.mjai_bot = libriichi.mjai.Bot(engine, self.seat) elif mode == GameMode.MJ3P: - settings = Settings() - if settings.enable_ot2_for_3p: - import riichi3p - self.mjai_bot = riichi3p.online.Bot(self.seat) - else: - import libriichi3p - self.mjai_bot = libriichi3p.mjai.Bot(engine, self.seat) + import libriichi3p + self.mjai_bot = libriichi3p.mjai.Bot(engine, self.seat) else: raise BotNotSupportingMode(mode) From 3ad14decfb71a7e1fb27a18e361b608c61e46369 Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Wed, 29 May 2024 14:59:29 +0800 Subject: [PATCH 19/21] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20OT2=20=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=9C=A8=E7=BA=BF=E7=8A=B6=E6=80=81=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/akagiot/bot_akagiot.py | 3 ++- bot/akagiot2/bot_akagiot2.py | 22 ++++++++++++++++++---- bot/bot.py | 5 +++++ bot/local/bot_local.py | 1 + bot/mjapi/bot_mjapi.py | 1 + bot_manager.py | 9 +++++++++ 6 files changed, 36 insertions(+), 5 deletions(-) diff --git a/bot/akagiot/bot_akagiot.py b/bot/akagiot/bot_akagiot.py index 1601c49..2a1a9f2 100644 --- a/bot/akagiot/bot_akagiot.py +++ b/bot/akagiot/bot_akagiot.py @@ -15,7 +15,8 @@ class BotAkagiOt(BotMjai): def __init__(self, url:str, apikey:str) -> None: super().__init__("Akagi Online Bot") self.url = url - self.apikey = apikey + self.apikey = apikey + self.model_type = "AkagiOT" self._check() diff --git a/bot/akagiot2/bot_akagiot2.py b/bot/akagiot2/bot_akagiot2.py index dfeccfb..dba689b 100644 --- a/bot/akagiot2/bot_akagiot2.py +++ b/bot/akagiot2/bot_akagiot2.py @@ -1,9 +1,8 @@ import multiprocessing +from pathlib import Path from bot.bot import BotMjai, GameMode from common.log_helper import LOGGER -from pathlib import Path - from common.utils import BotNotSupportingMode, Ot2BotCreationError model_file_path = "mjai/bot_3p/model.pth" @@ -15,7 +14,9 @@ class BotAkagiOt2(BotMjai): def __init__(self) -> None: super().__init__("Akagi OT2 Bot") self._supported_modes: list[GameMode] = [] + self._is_online = "Waiting" self._check() + self.model_type = "AkagiOT2" def _check(self): # check model file @@ -34,7 +35,7 @@ def supported_modes(self) -> list[GameMode]: """ return supported game modes""" return self._supported_modes - # 覆写父类方法 + # 覆写父类 impl 方法 def _init_bot_impl(self, mode: GameMode = GameMode.MJ3P): if mode == GameMode.MJ3P: try: @@ -47,13 +48,26 @@ def _init_bot_impl(self, mode: GameMode = GameMode.MJ3P): else: raise BotNotSupportingMode(mode) + # 覆写父类 react 方法 + def react(self, input_msg: dict) -> dict | None: + reaction = super().react(input_msg) + if reaction is not None: + if self.mjai_bot.is_online(): + self._is_online = "Online" + else: + self._is_online = "Offline" + return reaction + + @property + def is_online(self): + return self._is_online + # 尝试获取mjai.bot实例,该方法可能会导致 panick,需要在分离进程中使用 def create_bot_instance(queue): import riichi3p try: # 尝试创建一个mjai.bot实例 - riichi3p.online.Bot(1) queue.put(True) # 将成功的标志放入队列 except Exception as e: LOGGER.warning("Cannot create bot: %s", e, exc_info=True) diff --git a/bot/bot.py b/bot/bot.py index f4e8d17..f4b73d2 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -27,6 +27,11 @@ def __init__(self, name:str="Bot") -> None: self.name = name self._initialized:bool = False self.seat:int = None + self.model_type:str = None + + @property + def get_model_type(self) -> str: + return self.model_type @property def supported_modes(self) -> list[GameMode]: diff --git a/bot/local/bot_local.py b/bot/local/bot_local.py index d522b54..a0ccaed 100644 --- a/bot/local/bot_local.py +++ b/bot/local/bot_local.py @@ -18,6 +18,7 @@ def __init__(self, model_files:dict[GameMode, str]) -> None: super().__init__("Local Mortal Bot") self._supported_modes: list[GameMode] = [] self.model_files = model_files + self.model_type = "Local" self._engines:dict[GameMode, any] = {} for k,v in model_files.items(): if not Path(v).exists() or not Path(v).is_file(): diff --git a/bot/mjapi/bot_mjapi.py b/bot/mjapi/bot_mjapi.py index feaafb4..fe95cf6 100644 --- a/bot/mjapi/bot_mjapi.py +++ b/bot/mjapi/bot_mjapi.py @@ -26,6 +26,7 @@ def __init__(self, setting:Settings) -> None: self._login_or_reg() self.id = -1 self.ignore_next_turn_self_reach:bool = False + self.model_type = "mjapi" @property def info_str(self): diff --git a/bot_manager.py b/bot_manager.py index 19821ee..8cf6175 100644 --- a/bot_manager.py +++ b/bot_manager.py @@ -463,6 +463,15 @@ def _update_overlay_botleft(self): model_text = '🤖' if self.is_bot_created(): model_text += self.st.lan().MODEL + ": " + self.st.model_type + if self.bot.get_model_type == "AkagiOT2": + if self.bot.is_online == "Online": + model_text += "(🌐)" + elif self.bot.is_online == "Offline": + model_text += "(🔌)" + elif self.bot.is_online == "Waiting": + model_text += "(⏳)" + else: + model_text += "(❓)" else: model_text += self.st.lan().MODEL_NOT_LOADED From 40199c38442c3ebba98a7c25501a7a6430282c5e Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Wed, 29 May 2024 23:25:27 +0800 Subject: [PATCH 20/21] Update readme.md --- readme.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/readme.md b/readme.md index 6d92c06..62a5ac0 100644 --- a/readme.md +++ b/readme.md @@ -59,12 +59,48 @@ python main.py ``` ### 配置模型 本程序支持几种模型来源。其中,本地模型(Local)是基于 Akagi 兼容的 Mortal 模型。要获取 Akagi 的模型,请参见 Akagi Github 的说明。 + +#### 使用 Akagi OT2模型 如需使用 AkagiOT2 模型,请前往 Akagi 的官方 Discord 频道获取对应的 model.pth 和 riichi3p 包并安装。 + +第一步:下载模型文件 model.pth 和 riichi3p 包 + +第二步:将 model.pth 放置到目录 `mjai/bot_3p/` 下 + +第三步:安装 riichi3p 包 +```bash +pip install riichi3p-${version}.whl +``` +此处安装对应 python 版本和操作系统的 riichi3p 包。 + +第四步:运行 main.py,打开设置,在模型类型中选择 AkagiOT2 模型。 + +第五步:设置 OT2 的 url地址 和 api_key,并保存。 + +第六步:等待 OT2 模型加载完成,即可开始游戏。 + ### Model Configuration This program supports different types of AI models. The 'Local' Model type uses Mortal models compatible with Akagi. To acquire Akagi's models, please refer to Akagi Github . To use the AkagiOT2 model, please visit Akagi's official Discord channel to obtain the corresponding model.pth and riichi3p package, and then install them. +#### Using the Akagi OT2 Model +To use the AkagiOT2 model, please visit the official Akagi Discord channel to obtain the corresponding `model.pth` file and `riichi3p` package, and install them. + +Step 1: Download the model file `model.pth` and the `riichi3p` package. + +Step 2: Place the `model.pth` file in the directory `mjai/bot_3p/`. + +Step 3: Install the `riichi3p` package: +```bash +pip install riichi3p-${version}.whl +``` +Install the `riichi3p` package appropriate for your Python version and operating system here. + +Step 4: Run `main.py`, open the settings, and select the AkagiOT2 model under model type. + +Step 5: Set the URL and API key for OT2 and save. +Step 6: Wait for the OT2 model to load completely, then you can start the game. ## 截图 / Screenshots From 1154c6823c6817401f9361068325b6aa231a325f Mon Sep 17 00:00:00 2001 From: Calvin Div Date: Sat, 1 Jun 2024 22:01:09 +0800 Subject: [PATCH 21/21] =?UTF-8?q?bot=20=E5=8A=A0=E8=BD=BD=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=9A=84=E8=B6=85=E6=97=B6=E6=97=B6=E9=97=B4=E5=AE=BD?= =?UTF-8?q?=E9=99=90=E8=87=B3=2010=20=E7=A7=92=EF=BC=8C=E6=89=93=E5=8D=B0?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E9=94=99=E8=AF=AF=E3=80=82=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=9C=AA=E5=B0=9D=E8=AF=95=E5=8A=A0=E8=BD=BD=E7=9A=84=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/akagiot2/bot_akagiot2.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bot/akagiot2/bot_akagiot2.py b/bot/akagiot2/bot_akagiot2.py index dba689b..97fc3b8 100644 --- a/bot/akagiot2/bot_akagiot2.py +++ b/bot/akagiot2/bot_akagiot2.py @@ -4,6 +4,7 @@ from bot.bot import BotMjai, GameMode from common.log_helper import LOGGER from common.utils import BotNotSupportingMode, Ot2BotCreationError +import time model_file_path = "mjai/bot_3p/model.pth" @@ -68,6 +69,7 @@ def create_bot_instance(queue): import riichi3p try: # 尝试创建一个mjai.bot实例 + riichi3p.online.Bot(1) queue.put(True) # 将成功的标志放入队列 except Exception as e: LOGGER.warning("Cannot create bot: %s", e, exc_info=True) @@ -82,10 +84,17 @@ def try_create_ot2_bot(): process.start() # 尝试从队列中获取结果,设置超时时间防止无限等待 + start_time = time.time() + timeout = 10 try: - success = queue.get(timeout=3) + timeout = 10 + success = queue.get(timeout=timeout) except Exception as e: + end_time = time.time() LOGGER.error("Failed to retrieve the result from the subprocess: %s", e) + if end_time - start_time >= timeout: + LOGGER.error("Timeout when waiting for the result from the subprocess") + process.terminate() success = False process.join()