From 6865fa082f59433034fac8380f71c780e507deb0 Mon Sep 17 00:00:00 2001
From: ljzloser <1312358581@qq.com>
Date: Wed, 24 Dec 2025 18:25:36 +0800
Subject: [PATCH 1/7] =?UTF-8?q?fix(plugin):=20=E4=BF=AE=E5=A4=8D=E4=BA=8B?=
=?UTF-8?q?=E4=BB=B6=E5=A4=84=E7=90=86=E5=99=A8=E8=BF=94=E5=9B=9E=E7=B1=BB?=
=?UTF-8?q?=E5=9E=8B=E6=A3=80=E6=9F=A5=E5=8F=8A=E4=B8=8A=E4=B8=8B=E6=96=87?=
=?UTF-8?q?=E5=A4=84=E7=90=86=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
修复BasePlugin中事件处理器返回类型不一致的问题,添加类型检查
修正PluginManager和PluginManagerUI中的上下文处理逻辑
优化History插件的事件处理器返回
---
src/mp_plugins/base/plugin.py | 6 +++++-
src/mp_plugins/plugin_manager.py | 6 +++---
src/pluginDialog.py | 30 +++++++++++++++++-------------
src/plugins/History/History.py | 5 ++---
4 files changed, 27 insertions(+), 20 deletions(-)
diff --git a/src/mp_plugins/base/plugin.py b/src/mp_plugins/base/plugin.py
index 0d5fc95..cf314ff 100644
--- a/src/mp_plugins/base/plugin.py
+++ b/src/mp_plugins/base/plugin.py
@@ -151,6 +151,10 @@ def __message_dispatching(self, message: Message) -> None:
return
for handler in self._event_handlers[message.data.__class__]:
event = handler(self, message.data)
+ if event.__class__ != message.data.__class__:
+ raise TypeError(
+ f"Event handler must return the same event type got {event.__class__.__name__} expected {message.data.__class__.__name__}"
+ )
message.data = event
self.__message_queue.put(message)
else:
@@ -163,7 +167,7 @@ def __message_dispatching(self, message: Message) -> None:
if hasattr(self, "_context"):
self._context = message.data
self.initialize()
- self._context = message.class_name
+ self._context = message.data
elif message.mode == MessageMode.Error:
pass
elif message.mode == MessageMode.Unknown:
diff --git a/src/mp_plugins/plugin_manager.py b/src/mp_plugins/plugin_manager.py
index 1dbfb6e..add6e5d 100644
--- a/src/mp_plugins/plugin_manager.py
+++ b/src/mp_plugins/plugin_manager.py
@@ -56,7 +56,7 @@ def __init__(self) -> None:
self.__error_callback = None
self.__running = False
- self.__context: BaseContext = None
+ self.__context: BaseContext | None = None
# -------------------------
# 单例
@@ -78,7 +78,7 @@ def port(self):
@property
def context(self) -> BaseContext:
- return self.__context
+ return self.__context # type: ignore
@context.setter
def context(self, context: BaseContext):
@@ -230,7 +230,7 @@ def send_event(
if message.id in self.__event_dict:
del self.__event_dict[message.id]
- return result
+ return result # type: ignore
# -------------------------
# 发消息入口(入队)
diff --git a/src/pluginDialog.py b/src/pluginDialog.py
index fac2304..3cd8bd1 100644
--- a/src/pluginDialog.py
+++ b/src/pluginDialog.py
@@ -38,7 +38,7 @@
_translate = QCoreApplication.translate
-class PluginManagerUI(RoundQDialog):
+class PluginManagerUI(QDialog):
def __init__(self, plugin_names: list[str]):
"""
@@ -64,8 +64,10 @@ def __init__(self, plugin_names: list[str]):
# ================= 左侧插件列表 =================
self.list_widget = QListWidget()
for name in self.plugin_names:
- self.list_widget.addItem(
- PluginManager.instance().Get_Context_By_Name(name).display_name)
+ ctx = PluginManager.instance().Get_Context_By_Name(name)
+ if ctx is None:
+ continue
+ self.list_widget.addItem(ctx.display_name)
self.list_widget.currentRowChanged.connect(self.on_plugin_selected)
root_layout.addWidget(self.list_widget, 1)
@@ -167,6 +169,8 @@ def update_context(self, index: int):
ctx = PluginManager.instance().Get_Context_By_Name(
self.plugin_names[index])
+ if ctx is None:
+ return
# --- PluginContext 原样填充 ---
for key, value in msgspec.structs.asdict(ctx).items():
@@ -184,8 +188,11 @@ def load_settings(self, settings: Dict[str, BaseSetting]):
# 清空原控件
while self.scroll_layout.count():
item = self.scroll_layout.takeAt(0)
- if item.widget():
- item.widget().deleteLater()
+ if item is None:
+ continue
+ widget = item.widget()
+ if widget is not None:
+ widget.deleteLater()
self.current_settings_widgets.clear()
@@ -221,14 +228,11 @@ def load_settings(self, settings: Dict[str, BaseSetting]):
# 保存按钮
# ===================================================================
def on_save_clicked(self):
- ctx = self.plugin_contexts[self.list_widget.currentRow()]
-
- # --- PluginContext 原样填充 ---
- for key, label in self.detail_labels.items():
- value = getattr(ctx, key)
- if isinstance(value, list):
- value = ", ".join(value)
- label.setText(str(value))
+ ctx = PluginManager.instance().Get_Context_By_Name(
+ self.plugin_names[self.list_widget.currentRow()]
+ )
+ if ctx is None:
+ return
# --- 你自己的获取 settings 的函数 ---
settings_dict = PluginManager.instance().Get_Settings(ctx.name)
diff --git a/src/plugins/History/History.py b/src/plugins/History/History.py
index dafa66f..c83b40b 100644
--- a/src/plugins/History/History.py
+++ b/src/plugins/History/History.py
@@ -1,4 +1,3 @@
-
import sys
import os
import msgspec
@@ -78,8 +77,8 @@ def shutdown(self) -> None:
return super().shutdown()
@BasePlugin.event_handler(GameEndEvent)
- def on_game_end(self, event: GameEndEvent) -> None:
- pass
+ def on_game_end(self, event: GameEndEvent):
+ return event
if __name__ == "__main__":
From 9c3591557aa059fb99d4543538ef4720a655c150 Mon Sep 17 00:00:00 2001
From: ljzloser <1312358581@qq.com>
Date: Fri, 26 Dec 2025 01:05:01 +0800
Subject: [PATCH 2/7] =?UTF-8?q?feat(history):=20=E5=AE=9E=E7=8E=B0?=
=?UTF-8?q?=E6=B8=B8=E6=88=8F=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=E5=8F=8AGUI=E7=95=8C=E9=9D=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
新增历史记录数据库存储功能,完善GameEndEvent数据结构,添加历史记录查看GUI界面,支持播放和导出历史记录
---
.gitignore | 2 +
src/history_gui.py | 305 +++++++++++++++++++++++++++++++++
src/main.py | 26 +--
src/mineSweeperGUI.py | 20 ++-
src/mp_plugins/events.py | 39 ++++-
src/plugins/History/History.py | 165 +++++++++++++++---
src/utils.py | 216 ++++++++++++++++-------
7 files changed, 653 insertions(+), 120 deletions(-)
create mode 100644 src/history_gui.py
diff --git a/.gitignore b/.gitignore
index 568a6bc..3cf00b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -169,3 +169,5 @@ old/
src/plugins/*/*.json
src/plugins/*/*.db
.vscode/
+src/history.db
+history_show_fields.json
diff --git a/src/history_gui.py b/src/history_gui.py
new file mode 100644
index 0000000..7b54ce9
--- /dev/null
+++ b/src/history_gui.py
@@ -0,0 +1,305 @@
+from enum import Enum
+from fileinput import filename
+import json
+import os
+from pathlib import Path
+import sqlite3
+import subprocess
+import sys
+from typing import Any
+from PyQt5.QtGui import QCloseEvent
+from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QTableWidget, QMenu, \
+ QAction, QTableWidgetItem, QHeaderView, QTableView, QMessageBox, QFileDialog
+from PyQt5.QtCore import QDateTime, Qt, QCoreApplication
+from datetime import datetime
+import inspect
+from utils import GameBoardState, BaseDiaPlayEnum, get_paths, patch_env
+_translate = QCoreApplication.translate
+
+
+class HistoryData:
+ replay_id: int = 0
+ game_board_state: GameBoardState = GameBoardState.Win
+ rtime: float = 0
+ left: int = 0
+ right: int = 0
+ double: int = 0
+ left_s: float = 0.0
+ right_s: float = 0.0
+ double_s: float = 0.0
+ level: int = 0
+ cl: int = 0
+ cl_s: float = 0.0
+ ce: int = 0
+ ce_s: float = 0.0
+ rce: int = 0
+ lce: int = 0
+ dce: int = 0
+ bbbv: int = 0
+ bbbv_solved: int = 0
+ bbbv_s: float = 0.0
+ flag: int = 0
+ path: float = 0.0
+ etime: float = datetime.now()
+ start_time: datetime = datetime.now()
+ end_time: datetime = datetime.now()
+ mode: int = 0
+ software: str = ""
+ player_identifier: str = ""
+ race_identifier: str = ""
+ uniqueness_identifier: str = ""
+ stnb: float = 0.0
+ corr: float = 0.0
+ thrp: float = 0.0
+ ioe: float = 0.0
+ is_official: int = 0
+ is_fair: int = 0
+ op: int = 0
+ isl: int = 0
+
+ @classmethod
+ def fields(cls):
+ return [name for name, value in inspect.getmembers(cls) if not name.startswith("__") and not callable(value) and not name.startswith("_")]
+
+ @classmethod
+ def query_all(cls):
+ return f"select {','.join(cls.fields())} from history"
+
+ @classmethod
+ def from_dict(cls, data: dict):
+ instance = cls()
+ for name, value in inspect.getmembers(cls):
+ if not name.startswith("__") and not callable(value) and not name.startswith("_"):
+ new_value = data.get(name)
+ if isinstance(value, datetime):
+ value = datetime.fromtimestamp(new_value / 1_000_000)
+ elif isinstance(value, float):
+ value = round(new_value, 4)
+ elif isinstance(value, BaseDiaPlayEnum):
+ value = value.__class__(new_value)
+ else:
+ value = new_value
+ setattr(instance, name, value)
+ return instance
+
+
+class HistoryTable(QWidget):
+ def __init__(self, showFields: set[str], parent: QWidget | None = ...) -> None:
+ super().__init__(parent)
+ self.layout: QVBoxLayout = QVBoxLayout(self)
+ self.table = QTableWidget(self)
+ self.layout.addWidget(self.table)
+ self.setLayout(self.layout)
+ # 设置不可编辑
+ self.table.setEditTriggers(QTableWidget.NoEditTriggers)
+ # 添加右键菜单
+ self.table.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.table.customContextMenuRequested.connect(self.show_context_menu)
+ self.showFields: set[str] = showFields
+ self.headers = [
+ "replay_id",
+ "game_board_state",
+ "rtime",
+ "left",
+ "right",
+ "double",
+ "left_s",
+ "right_s",
+ "double_s",
+ "level",
+ "cl",
+ "cl_s",
+ "ce",
+ "ce_s",
+ "rce",
+ "lce",
+ "dce",
+ "bbbv",
+ "bbbv_solved",
+ "bbbv_s",
+ "flag",
+ "path",
+ "etime",
+ "start_time",
+ "end_time",
+ "mode",
+ "software",
+ "player_identifier",
+ "race_identifier",
+ "uniqueness_identifier",
+ "stnb",
+ "corr",
+ "thrp",
+ "ioe",
+ "is_official",
+ "is_fair",
+ "op",
+ "isl",
+ ]
+ self.table.setColumnCount(len(self.showFields))
+ self.table.setHorizontalHeaderLabels(self.headers)
+ # 居中显示文字
+ self.table.horizontalHeader().setDefaultAlignment(Qt.AlignCenter)
+ # 选中整行
+ self.table.setSelectionBehavior(QTableView.SelectRows)
+
+ # 自适应列宽
+ self.table.horizontalHeader().setSectionResizeMode(
+ QHeaderView.ResizeToContents)
+ # 初始化隐藏列
+ for i, field in enumerate(self.headers):
+ self.table.setColumnHidden(i, field not in self.showFields)
+
+ def load(self, data: list[HistoryData]):
+ self.table.setRowCount(len(data))
+ for i, row in enumerate(data):
+ for j, field in enumerate(self.headers):
+ value = getattr(row, field)
+
+ self.table.setItem(i, j, self.build_item(value))
+
+ def build_item(self, value: Any):
+ if isinstance(value, datetime):
+ new_value = value.strftime("%Y-%m-%d %H:%M:%S.%f")
+ if isinstance(value, BaseDiaPlayEnum):
+ new_value = value.display_name
+ else:
+ new_value = value
+ item = QTableWidgetItem(str(new_value))
+ item.setData(Qt.UserRole, value)
+ item.setTextAlignment(Qt.AlignCenter | Qt.AlignVCenter)
+ return item
+
+ def refresh(self):
+ parent: 'HistoryGUI' = self.parent()
+ parent.load_data()
+
+ def show_context_menu(self, pos):
+ menu = QMenu(self)
+ action1 = menu.addAction(_translate("Form", "播放"), self.play_row)
+ action2 = menu.addAction(_translate("Form", "导出"), self.export_row)
+ action3 = menu.addAction(_translate("Form", "刷新"), self.refresh)
+ # 给action3添加子菜单
+ submenu = QMenu(_translate("Form", "显示字段"), self)
+ # 遍历所有字段,添加一个action
+ for field in self.headers:
+ action = QAction(field, self)
+ action.setCheckable(True)
+ action.setChecked(field in self.showFields)
+ action.triggered.connect(
+ lambda checked: self.on_action_triggered(checked))
+ submenu.addAction(action)
+ menu.addMenu(submenu)
+ menu.exec_(self.table.mapToGlobal(pos))
+
+ def on_action_triggered(self, checked: bool):
+ action: QAction = self.sender()
+ name = action.text()
+ self.table.setColumnHidden(
+ self.table.horizontalHeader().logicalIndex(
+ self.headers.index(name)), not checked)
+ if checked:
+ self.showFields.add(name)
+ else:
+ self.showFields.remove(name)
+
+ def save_evf(self, evf_path: str):
+ row_index = self.table.currentRow()
+ if row_index < 0:
+ return
+ row = self.table.item(row_index, 0).data(Qt.UserRole)
+ for filed in self.headers:
+ if filed == "replay_id":
+ replay_id = self.table.item(
+ row_index, self.headers.index(filed)).data(Qt.UserRole)
+ conn = sqlite3.connect(Path(get_paths()) / "history.db")
+ conn.row_factory = sqlite3.Row # 设置行工厂
+ cursor = conn.cursor()
+ cursor.execute(
+ "select raw_data from history where replay_id = ?", (replay_id,))
+
+ raw_data = cursor.fetchone()[0]
+ with open(evf_path, "wb") as f:
+ f.write(raw_data)
+ conn.close()
+
+ def play_row(self):
+ temp_filename = Path(get_paths())/f"tmp.evf"
+ self.save_evf(temp_filename)
+ # 检查当前目录是否存在main.py
+ if (Path(get_paths()) / "main.py").exists():
+ subprocess.Popen(
+ [
+ sys.executable,
+ str(Path(get_paths()) / "main.py"),
+ temp_filename
+ ],
+ env=patch_env(),
+ )
+ elif (Path(get_paths()) / "metaminesweeper.exe").exists():
+ subprocess.Popen(
+ [
+ Path(get_paths()) / "metaminesweeper.exe",
+ temp_filename
+ ]
+ )
+ else:
+ QMessageBox.warning(
+ self, "错误", "当前目录下不存在main.py或metaminesweeper.exe")
+ return
+
+ def export_row(self):
+ file_path, _ = QFileDialog.getSaveFileName(self, _translate(
+ "Form", "导出evf文件"), get_paths(), "evf文件 (*.evf)")
+
+ if not file_path:
+ return
+ self.save_evf(file_path)
+
+
+class HistoryGUI(QWidget):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle(_translate("Form", "历史记录"))
+ self.resize(800, 600)
+ self.layout = QVBoxLayout(self)
+ self.table = HistoryTable(self.get_show_fields(), self)
+ self.layout.addWidget(self.table)
+ self.setLayout(self.layout)
+ self.load_data()
+
+ def load_data(self):
+ conn = sqlite3.connect(Path(get_paths()) / "history.db")
+ conn.row_factory = sqlite3.Row # 设置行工厂
+ cursor = conn.cursor()
+ cursor.execute(HistoryData.query_all())
+ datas = cursor.fetchall()
+ history_data = [HistoryData.from_dict(dict(data)) for data in datas]
+ self.table.load(history_data)
+
+ @property
+ def config_path(self):
+ return Path(get_paths()) / "history_show_fields.json"
+
+ def get_show_fields(self):
+ # 先判断是否存在展示列的json文件
+ if not (self.config_path).exists():
+ return set(HistoryData.fields())
+ with open(self.config_path, "r") as f:
+ return set(json.load(f))
+
+ def closeEvent(self, a0: QCloseEvent | None) -> None:
+ with open(self.config_path, "w") as f:
+ json.dump(list(self.table.showFields), f)
+ return super().closeEvent(a0)
+
+
+if __name__ == "__main__":
+
+ app = QApplication(sys.argv)
+
+ gui = HistoryGUI()
+
+ gui.show()
+
+ sys.exit(app.exec_())
diff --git a/src/main.py b/src/main.py
index d4a6049..0bec943 100644
--- a/src/main.py
+++ b/src/main.py
@@ -16,6 +16,7 @@
from mp_plugins import PluginManager
from pathlib import Path
# import os
+from utils import get_paths, patch_env
os.environ["QT_FONT_DPI"] = "96"
@@ -28,31 +29,6 @@
# root = os.path.dirname(os.path.abspath(__file__)) # 你的项目根目录
# env["PYTHONPATH"] = root
# return env
-def get_paths():
- if getattr(sys, "frozen", False):
- # 打包成 exe
- dir = os.path.dirname(sys.executable) # exe 所在目录
- else:
- dir = os.path.dirname(os.path.abspath(__file__))
-
- return dir
-
-
-def patch_env():
- import os
- import sys
-
- env = os.environ.copy()
-
- if getattr(sys, "frozen", False):
- # 打包成 exe,库解压到 _MEIPASS
- root = getattr(sys, "_MEIPASS", None)
- else:
- # 调试模式,库在项目目录
- root = os.path.dirname(os.path.abspath(__file__))
-
- env["PYTHONPATH"] = root
- return env
def on_new_connection(localServer: QLocalServer):
diff --git a/src/mineSweeperGUI.py b/src/mineSweeperGUI.py
index 4eef9fb..e39f8a3 100644
--- a/src/mineSweeperGUI.py
+++ b/src/mineSweeperGUI.py
@@ -1,6 +1,8 @@
+import base64
from PyQt5 import QtCore
from PyQt5.QtCore import QTimer, QCoreApplication, Qt, QRect, QUrl
from PyQt5.QtGui import QPixmap, QDesktopServices
+import msgspec
# from PyQt5.QtWidgets import QLineEdit, QInputDialog, QShortcut
# from PyQt5.QtWidgets import QApplication, QFileDialog, QWidget
import gameDefinedParameter
@@ -91,7 +93,6 @@ def save_evf_file_integrated():
self.action_open_ini.triggered.connect(
lambda: QDesktopServices.openUrl(QUrl.fromLocalFile(str(self.setting_path))))
-
self.frameShortcut1.activated.connect(lambda: self.predefined_Board(1))
self.frameShortcut2.activated.connect(lambda: self.predefined_Board(2))
self.frameShortcut3.activated.connect(lambda: self.predefined_Board(3))
@@ -559,8 +560,20 @@ def gameFinished(self):
self.score_board_manager.show(self.label.ms_board, index_type=2)
self.enable_screenshot()
self.unlimit_cursor()
- event = GameEndEvent()
- PluginManager.instance().send_event(event, response_count=0)
+ ms_board = self.label.ms_board
+ status = utils.GameBoardState(ms_board.game_board_state)
+ if status == utils.GameBoardState.Win:
+ self.dump_evf_file_data()
+ event = GameEndEvent()
+ data = msgspec.structs.asdict(event)
+ for key in data:
+ if hasattr(ms_board, key):
+ if key == "raw_data":
+ data[key] = base64.b64encode(
+ ms_board.raw_data).decode("utf-8")
+ data[key] = getattr(ms_board, key)
+ event = GameEndEvent(**data)
+ PluginManager.instance().send_event(event, response_count=0)
def gameWin(self): # 成功后改脸和状态变量,停时间
self.timer_10ms.stop()
@@ -958,7 +971,6 @@ def showTime(self, t):
elif t >= 1000:
return
-
def predefined_Board(self, k):
# 按快捷键123456时的回调
row = self.predefinedBoardPara[k]['row']
diff --git a/src/mp_plugins/events.py b/src/mp_plugins/events.py
index 0a23bcf..67758eb 100644
--- a/src/mp_plugins/events.py
+++ b/src/mp_plugins/events.py
@@ -2,4 +2,41 @@
class GameEndEvent(BaseEvent):
- pass
+ game_board_state: int = 0
+ rtime: float = 0
+ left: int = 126
+ right: int = 11
+ double: int = 14
+ left_s: float = 2.5583756345177666
+ right_s: float = 0.2233502538071066
+ double_s: float = 0.28426395939086296
+ level: int = 5
+ cl: int = 151
+ cl_s: float = 3.065989847715736
+ ce: int = 144
+ ce_s: float = 2.9238578680203045
+ rce: int = 11
+ lce: int = 119
+ dce: int = 14
+ bbbv: int = 127
+ bbbv_solved: int = 127
+ bbbv_s: float = 2.5786802030456855
+ flag: int = 11
+ path: float = 6082.352554578606
+ etime: float = 1666124184868000
+ start_time: int = 1666124135606000
+ end_time: int = 1666124184868000
+ mode: int = 0
+ software: str = "Arbiter"
+ player_identifier: str = "Wang Jianing G01825"
+ race_identifier: str = ""
+ uniqueness_identifier: str = ""
+ stnb: float = 0
+ corr: float = 0
+ thrp: float = 0
+ ioe: float = 0
+ is_official: int = 0
+ is_fair: int = 0
+ op: int = 0
+ isl: int = 0
+ raw_data: str = ''
diff --git a/src/plugins/History/History.py b/src/plugins/History/History.py
index c83b40b..018a3e6 100644
--- a/src/plugins/History/History.py
+++ b/src/plugins/History/History.py
@@ -1,3 +1,4 @@
+import base64
import sys
import os
import msgspec
@@ -36,40 +37,62 @@ def build_plugin_context(self) -> None:
self._plugin_context.description = "History"
self._plugin_context.author = "ljzloser"
+ @property
+ def db_path(self):
+ return self.path.parent.parent / "history.db"
+
def initialize(self) -> None:
- db_path = self.path / "history.db"
- if not db_path.exists():
- conn = sqlite3.connect(db_path)
+ if not self.db_path.exists():
+ conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
+
cursor.execute(
"""
-create table main.history
+create table history
(
- replay_id INTEGER
- primary key,
- termination_type INTEGER not null,
- player_nick TEXT not null,
- level TEXT not null,
- nf BOOLEAN not null,
- timeth INTEGER not null,
- bbbv_board INTEGER not null,
- bbbv_completed INTEGER not null
+ replay_id INTEGER primary key,
+ game_board_state INTEGER,
+ rtime REAL,
+ left INTEGER,
+ right INTEGER,
+ double INTEGER,
+ left_s REAL,
+ right_s REAL,
+ double_s REAL,
+ level INTEGER,
+ cl INTEGER,
+ cl_s REAL,
+ ce INTEGER,
+ ce_s REAL,
+ rce INTEGER,
+ lce INTEGER,
+ dce INTEGER,
+ bbbv INTEGER,
+ bbbv_solved INTEGER,
+ bbbv_s REAL,
+ flag INTEGER,
+ path REAL,
+ etime INTEGER,
+ start_time INTEGER,
+ end_time INTEGER,
+ mode INTEGER,
+ software TEXT,
+ player_identifier TEXT,
+ race_identifier TEXT,
+ uniqueness_identifier TEXT,
+ stnb REAL,
+ corr REAL,
+ thrp REAL,
+ ioe REAL,
+ is_official INTEGER,
+ is_fair INTEGER,
+ op INTEGER,
+ isl INTEGER,
+ raw_data BLOB
);
"""
)
conn.commit()
- cursor.execute(
- """
-create table main.replays
-(
- id INTEGER
- primary key,
- date DATETIME not null,
- replay BLOB not null,
- processed BOOLEAN default FALSE not null
-);
- """
- )
conn.close()
return super().initialize()
@@ -78,6 +101,98 @@ def shutdown(self) -> None:
@BasePlugin.event_handler(GameEndEvent)
def on_game_end(self, event: GameEndEvent):
+ data = msgspec.structs.asdict(event)
+ data["raw_data"] = base64.b64decode(s=event.raw_data)
+ conn = sqlite3.connect(self.db_path)
+ cursor = conn.cursor()
+ cursor.execute(
+ """
+insert into main.history
+(
+ game_board_state,
+ rtime,
+ left,
+ right,
+ double,
+ left_s,
+ right_s,
+ double_s,
+ level,
+ cl,
+ cl_s,
+ ce,
+ ce_s,
+ rce,
+ lce,
+ dce,
+ bbbv,
+ bbbv_solved,
+ bbbv_s,
+ flag,
+ path,
+ etime,
+ start_time,
+ end_time,
+ mode,
+ software,
+ player_identifier,
+ race_identifier,
+ uniqueness_identifier,
+ stnb,
+ corr,
+ thrp,
+ ioe,
+ is_official,
+ is_fair,
+ op,
+ isl,
+ raw_data
+ )
+values
+(
+ :game_board_state,
+ :rtime,
+ :left,
+ :right,
+ :double,
+ :left_s,
+ :right_s,
+ :double_s,
+ :level,
+ :cl,
+ :cl_s,
+ :ce,
+ :ce_s,
+ :rce,
+ :lce,
+ :dce,
+ :bbbv,
+ :bbbv_solved,
+ :bbbv_s,
+ :flag,
+ :path,
+ :etime,
+ :start_time,
+ :end_time,
+ :mode,
+ :software,
+ :player_identifier,
+ :race_identifier,
+ :uniqueness_identifier,
+ :stnb,
+ :corr,
+ :thrp,
+ :ioe,
+ :is_official,
+ :is_fair,
+ :op,
+ :isl,
+ :raw_data
+ )
+ """,
+ data)
+ conn.commit()
+ conn.close()
return event
diff --git a/src/utils.py b/src/utils.py
index 61d5ff8..c560e6c 100644
--- a/src/utils.py
+++ b/src/utils.py
@@ -1,6 +1,8 @@
# author : Wang Jianing(18201)
+import os
from random import shuffle, choice
# from random import randint, seed, sample
+import sys
from typing import List, Tuple, Union
# import time
from safe_eval import safe_eval
@@ -9,12 +11,94 @@
import ms_toollib as ms
import math
+from enum import Enum
+from PyQt5.QtCore import QCoreApplication
+
+_translate = QCoreApplication.translate
+
+
+class BaseDiaPlayEnum(Enum):
+
+ @property
+ def display_name(self):
+ return self.name
+
+ @classmethod
+ def from_display_name(cls, display_name: str):
+ for member in cls:
+ if member.display_name == display_name:
+ return member
+ raise ValueError(f"Invalid display name: {display_name}")
+
+ @classmethod
+ def display_names(cls):
+ return [member.display_name for member in cls]
+
+
+class GameBoardState(BaseDiaPlayEnum):
+ # GameBoardState::Ready => Ok(1),
+ # GameBoardState::Playing => Ok(2),
+ # GameBoardState::Win => Ok(3),
+ # GameBoardState::Loss => Ok(4),
+ # GameBoardState::PreFlaging => Ok(5),
+ # GameBoardState::Display => Ok(6),
+ Ready = 1
+ Playing = 2
+ Win = 3
+ Loss = 4
+ PreFlaging = 5
+ Display = 6
+
+ @property
+ def display_name(self):
+ match self:
+ case GameBoardState.Win:
+ return _translate("Form", "胜利")
+ case GameBoardState.Loss:
+ return _translate("Form", "失败")
+ case GameBoardState.Ready:
+ return _translate("Form", "准备")
+ case GameBoardState.Playing:
+ return _translate("Form", "进行中")
+ case GameBoardState.PreFlaging:
+ return _translate("Form", "预标记")
+ case GameBoardState.Display:
+ return _translate("Form", "显示")
+
+
+def get_paths():
+ if getattr(sys, "frozen", False):
+ # 打包成 exe
+ dir = os.path.dirname(sys.executable) # exe 所在目录
+ else:
+ dir = os.path.dirname(os.path.abspath(__file__))
+
+ return dir
+
+
+def patch_env():
+ import os
+ import sys
+
+ env = os.environ.copy()
+
+ if getattr(sys, "frozen", False):
+ # 打包成 exe,库解压到 _MEIPASS
+ root = getattr(sys, "_MEIPASS", None)
+ else:
+ # 调试模式,库在项目目录
+ root = os.path.dirname(os.path.abspath(__file__))
+
+ env["PYTHONPATH"] = root
+ return env
+
# OutputEnable = 0
# seedNum = 60223
EnuLimit = 50
assert EnuLimit >= 50
+
def choose_3BV(board_constraint, attempt_times_limit, params):
def choose_3BV_laymine(laymine):
if not board_constraint:
@@ -31,7 +115,7 @@ def choose_3BV_laymine(laymine):
b, success_flag = b
else:
success_flag = True
-
+
constraints = {}
wrapper_b = ms.Board(b)
if "bbbv" in board_constraint:
@@ -59,7 +143,8 @@ def choose_3BV_laymine(laymine):
if "cell8" in board_constraint:
constraints.update({"cell8": wrapper_b.cell8})
try:
- expression_flag = safe_eval(board_constraint, globals=constraints)
+ expression_flag = safe_eval(
+ board_constraint, globals=constraints)
except:
return (b, success_flag)
if expression_flag:
@@ -67,26 +152,31 @@ def choose_3BV_laymine(laymine):
t += 1
return (b, success_flag)
return choose_3BV_laymine
-
+
# 此处的board,看似是函数,实际由于装饰器的缘故是一个局面的列表
+
+
def laymine_solvable_thread(board_constraint, attempt_times_limit, params):
@choose_3BV(board_constraint, attempt_times_limit, params)
def board(pp):
return ms.laymine_solvable_thread(*pp)
return board
+
def laymine(board_constraint, attempt_times_limit, params):
@choose_3BV(board_constraint, attempt_times_limit, params)
def board(pp):
return ms.laymine(*pp)
return board
+
def laymine_op(board_constraint, attempt_times_limit, params):
@choose_3BV(board_constraint, attempt_times_limit, params)
def board(pp):
return ms.laymine_op(*pp)
return board
+
def laymine_solvable_adjust(board_constraint, attempt_times_limit, params):
# 暂时用不了
@choose_3BV(board_constraint, attempt_times_limit, params)
@@ -94,10 +184,11 @@ def board(pp):
return ms.laymine_solvable_adjust(*pp)
return board
+
def get_mine_times_limit(row: int, column: int):
'''
计算局面的雷数上限和尝试次数上限。当雷数小于等于雷数上限时,才可以用筛选法(考虑游戏体验)。
-
+
Parameters
----------
row : int
@@ -127,16 +218,18 @@ def get_mine_times_limit(row: int, column: int):
else:
return (int(area * 0.2) + 2, 10000)
+
def laymine_solvable_auto(row, column, mine_num, x, y):
# 自动选择方式的无猜埋雷
(max_mine_num, max_times) = get_mine_times_limit(row, column)
if mine_num <= max_mine_num:
- ans = ms.laymine_solvable_thread(row, column, mine_num, x, y, max_times)
+ ans = ms.laymine_solvable_thread(
+ row, column, mine_num, x, y, max_times)
if ans[1]:
return ans
return ms.laymine_solvable_adjust(row, column, mine_num, x, y)
-
-
+
+
def laymine_solvable(board_constraint, attempt_times_limit, params):
@choose_3BV(board_constraint, attempt_times_limit, params)
def board(pp):
@@ -147,29 +240,29 @@ def board(pp):
# poses是将要打开的多个位置,尝试调整board,使得game_board不变,而这些位置不再是雷。
# poses中至少有一个踩雷了。poses必须由同一个操作引起,例如单次双击
# 返回修改后的board和成功标识位
-def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]],
+def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]],
game_board: List[List[int]],
poses: List[Tuple[int, int]]) -> Tuple[List[List[int]], bool]:
"""
根据游戏板面情况,对局面进行枚举。
-
+
Args:
board (List[List[int]]): 原始的游戏板面,其中-1表示雷,非负整数表示周围雷的数量。
game_board (List[List[int]]): 当前的游戏板面,其中10表示未知,11表示必然为雷,非负整数表示周围雷的数量。
poses (List[Tuple[int, int]]): 需要枚举的坐标点列表。
-
+
Returns:
Tuple[List[List[int]], bool]:
- List[List[int]]: 枚举后的游戏板面,其中-1表示雷,非负整数表示周围雷的数量。
- bool: 枚举是否成功,如果成功返回True,否则返回False。
-
+
Raises:
TypeError: 如果board不是list类型,会尝试将其转换为二维向量,如果转换失败则抛出TypeError。
-
+
"""
if not isinstance(board, list):
- board = board.into_vec_vec()
- if all([board[x][y] != -1 for x,y in poses]):
+ board = board.into_vec_vec()
+ if all([board[x][y] != -1 for x, y in poses]):
# 全不是雷
return board, True
for i in range(len(board)):
@@ -177,12 +270,12 @@ def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]],
if game_board[i][j] == 11:
game_board[i][j] = 10
game_board = ms.mark_board(game_board, remark=True)
- if any([game_board[x][y] == 11 for x,y in poses]):
+ if any([game_board[x][y] == 11 for x, y in poses]):
# 有一个必然是雷,就直接返回
return board, False
# 删去12不用管
poses = list(filter(lambda xy: game_board[xy[0]][xy[1]] == 10, poses))
-
+
# 第一步,将board上的10分成三份,不变区0、无约束区1、数字约束区2
# 引入定理一:假如poses数量多于1,则必然全都在数字约束区
# 定理二:poses必然全部在同一个段中,要么全不在
@@ -194,18 +287,19 @@ def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]],
rand_blank_num = 0
constraint_mine_num = 0
constraint_blank_num = 0
-
+
matrix_ases, matrix_xses, matrix_bses = ms.refresh_matrixses(game_board)
for idb, block in enumerate(matrix_xses):
for idl, line in enumerate(block):
if poses[0] in line:
- if len(line) >= EnuLimit:#枚举法的极限
- #超过枚举极限时,暂时不能给出可能的解,有待升级
+ if len(line) >= EnuLimit: # 枚举法的极限
+ # 超过枚举极限时,暂时不能给出可能的解,有待升级
return board, False
matrix_a = matrix_ases[idb][idl]
matrix_x = matrix_xses[idb][idl]
matrix_b = matrix_bses[idb][idl]
- constraint_mine_num = [board[x][y] for x,y in matrix_x].count(-1)
+ constraint_mine_num = [board[x][y]
+ for x, y in matrix_x].count(-1)
constraint_blank_num = len(matrix_x) - constraint_mine_num
for (i, j) in line:
type_board[i][j] = 2
@@ -238,10 +332,10 @@ def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]],
constraint_mine_num_min = max(constraint_mine_num - rand_blank_num, 0)
all_solution = ms.cal_all_solution(matrix_a, matrix_b)
idposes = [matrix_x.index(pos) for pos in poses]
- all_solution = filter(lambda x: constraint_mine_num_min <= x.count(1) <=\
- constraint_mine_num_max and\
- all([x[idpos] != 1 for idpos in idposes]),
- all_solution)
+ all_solution = filter(lambda x: constraint_mine_num_min <= x.count(1) <=
+ constraint_mine_num_max and
+ all([x[idpos] != 1 for idpos in idposes]),
+ all_solution)
all_solution = list(all_solution)
if not all_solution:
return board, False
@@ -273,13 +367,13 @@ def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]],
def trans_expression(expression: str):
"""
将输入的表达式字符串进行一系列替换处理。
-
+
Args:
expression (str): 待处理的表达式字符串。
-
+
Returns:
str: 处理后的表达式字符串。
-
+
具体处理规则如下:
1. 将表达式转换为小写,并去除首尾空白字符,且仅保留前10000个字符。
2. 将表达式中的"3bv"替换为"bbbv"。
@@ -304,16 +398,16 @@ def trans_expression(expression: str):
def trans_game_mode(mode: int) -> str:
"""
将游戏模式数字转换为对应的中文描述。
-
+
Args:
mode (int): 游戏模式数字,取值范围在0到10之间。
-
+
Returns:
str: 返回对应的中文游戏模式描述。
-
+
Raises:
ValueError: 如果mode不在0到10的范围内,将抛出异常。
-
+
"""
_translate = QtCore.QCoreApplication.translate
if mode == 0:
@@ -338,44 +432,45 @@ def trans_game_mode(mode: int) -> str:
return _translate("Form", '强可猜')
elif mode == 10:
return _translate("Form", '弱可猜')
-
+
# class abstract_game_board(object):
# __slots__ = ('game_board', 'mouse_state', 'game_board_state')
# def reset(self, *args):
# ...
# def step(self, *args):
# ...
-
-
+
+
class CoreBaseVideo(ms.BaseVideo):
mouse_state = 1
game_board_state = 1
x_y = (0, 0)
+
def __new__(cls, board, cell_pixel_size):
return ms.BaseVideo.__new__(cls, board, cell_pixel_size)
+
def __init__(self, board, cell_pixel_size):
super(CoreBaseVideo, self).__init__()
+
@property
def game_board(self):
return self._game_board
+
@game_board.setter
def game_board(self, game_board):
self._game_board = game_board
- class AlwaysZero:
- def __getitem__(self, key):
+
+ class AlwaysZero:
+ def __getitem__(self, key):
class Inner:
- def __getitem__(self, inner_key):
- return 0
+ def __getitem__(self, inner_key):
+ return 0
return Inner()
# self.timer_video.stop()以后,槽函数可能还在跑
# self.label.ms_board就会变成abstract_game_board
# 使game_board_poss[x][y]永远返回0。否则此处就会报错
game_board_poss = AlwaysZero()
-
-
-
-
# unsolvableStructure = ms_toollib.py_unsolvableStructure
# unsolvableStructure2(BoardCheck)
@@ -384,30 +479,30 @@ def __getitem__(self, inner_key):
# 局面至少大于4*4
# 返回0或1
-def print2(arr, mode = 0):
+def print2(arr, mode=0):
# 调试时便于打印 print2(BoardofGame)
if mode == 0:
for i in arr:
for j in i:
- print('%2.d'%j, end=', ')
+ print('%2.d' % j, end=', ')
print()
elif mode == 1:
for i in arr:
for j in i:
- print('%2.d'%j.num, end=', ')
+ print('%2.d' % j.num, end=', ')
print()
elif mode == 2:
for i in arr:
for j in i:
- print('%2.d'%j.status, end=', ')
+ print('%2.d' % j.status, end=', ')
print()
+
def debug_ms_board(ms_board):
for i in range(ms_board.events_len):
print(f"{ms_board.events_time(i)}: '{ms_board.events_mouse(i)}', ({ms_board.events_y(i)}, {ms_board.events_x(i)})")
-
# def updata_ini(file_name: str, data):
# conf = configparser.ConfigParser()
# conf.read(file_name, encoding='utf-8')
@@ -430,7 +525,6 @@ def main():
# time2 = time.time()
# print(time2 - time1)
-
# # 测试埋雷+计算3BV速度算例
# time1 = time.time()
# for i in range(100000):
@@ -456,9 +550,6 @@ def main():
# print(time2 - time1)
# print(num/T)
-
-
-
# import minepy
# import numpy as np
# for ii in range(10):
@@ -473,10 +564,10 @@ def main():
# mine = minepy.MINE(alpha=0.6, c=15, est="mic_approx")
# mine.compute_score(x, y)
# print(mine.mic())
-
+
# a = laymine_solvable(0, 10, 100000, (8, 8, 10, 0, 0, 100000))
# print(a)
-
+
# game_board = [[ 0, 1,10,10,10,10, 1, 0],
# [ 1, 3,10,10,10,10, 3, 1],
# [ 1,10,10,10,10,10,10, 1],
@@ -495,7 +586,7 @@ def main():
# [ 1, 3, 5, 6,-1, 5, 3, 1],
# [ 0, 1,-1,-1, 3,-1, 1, 0],
# ]
-
+
# game_board = [[10,10,10,10,10,10,10,10],
# [10,10,10,10,10,10,10,10],
# [10,10,10,10,10,10,10,10],
@@ -514,11 +605,11 @@ def main():
# [ 0, 0, 0, 0, 0, 0, 0, 0],
# [ 0, 0, 0, 0, 0, 0, 0, 0],
# ]
-
+
# print2(enumerateChangeBoard2(board, game_board, [(2, 3), (3, 2), (2, 2)])[0])
-
+
constraints = {}
- board_constraint="all([1,2,3])"
+ board_constraint = "all([1,2,3])"
if "bbbv" in board_constraint:
constraints.update({"bbbv": 120})
try:
@@ -526,15 +617,10 @@ def main():
print(expression_flag)
except:
print("wrong")
-
- ...
-
-if __name__ == '__main__':
-
- main()
-
-
+ ...
+if __name__ == '__main__':
+ main()
From 68fb35abe72079ec807d5a874ce80c2199e2ac5e Mon Sep 17 00:00:00 2001
From: ljzloser <1312358581@qq.com>
Date: Fri, 26 Dec 2025 01:25:15 +0800
Subject: [PATCH 3/7] =?UTF-8?q?feat(History):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E4=BF=9D=E5=AD=98=E6=A8=A1?=
=?UTF-8?q?=E5=BC=8F=E9=85=8D=E7=BD=AE=E9=80=89=E9=A1=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/plugins/History/History.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/plugins/History/History.py b/src/plugins/History/History.py
index 018a3e6..ce6a364 100644
--- a/src/plugins/History/History.py
+++ b/src/plugins/History/History.py
@@ -19,7 +19,7 @@
class HistoryConfig(BaseConfig):
- pass
+ save_mode: SelectSetting
class History(BasePlugin):
@@ -28,7 +28,11 @@ def __init__(
) -> None:
super().__init__()
self._context: AppContext
- self._config = HistoryConfig()
+ self._config = HistoryConfig(
+ save_mode=SelectSetting(
+ "保存模式", "仅保存胜利局", options=["仅保存胜利局"]
+ )
+ )
def build_plugin_context(self) -> None:
self._plugin_context.name = "History"
From 7bf23d6314a5f64fc95aa071aab3b789b857fd06 Mon Sep 17 00:00:00 2001
From: ljzloser <1312358581@qq.com>
Date: Fri, 26 Dec 2025 01:34:50 +0800
Subject: [PATCH 4/7] =?UTF-8?q?refactor(plugin):=20=E5=B0=86=E6=8F=92?=
=?UTF-8?q?=E4=BB=B6=E5=88=9D=E5=A7=8B=E5=8C=96=E9=80=BB=E8=BE=91=E7=A7=BB?=
=?UTF-8?q?=E5=88=B0=E6=9C=8D=E5=8A=A1=E5=99=A8=E8=BF=9E=E6=8E=A5=E4=B9=8B?=
=?UTF-8?q?=E5=90=8E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
将插件管理器和应用上下文的初始化代码从主程序入口移动到本地服务连接建立之后,确保插件加载在正确的上下文中执行
---
src/main.py | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/src/main.py b/src/main.py
index 0bec943..bc4de51 100644
--- a/src/main.py
+++ b/src/main.py
@@ -135,14 +135,6 @@ def cli_check_file(file_path: str) -> int:
if args.check:
exit_code = cli_check_file(args.check)
sys.exit(exit_code)
- env = patch_env()
- context = AppContext(name="Metasweeper", version="1.0.0", display_name="元扫雷",
- plugin_dir=(Path(get_paths()) / "plugins").as_posix(),
- app_dir=get_paths()
- )
- PluginManager.instance().context = context
-
- PluginManager.instance().start(Path(get_paths()) / "plugins", env)
app = QtWidgets.QApplication(sys.argv)
serverName = "MineSweeperServer"
@@ -161,6 +153,15 @@ def cli_check_file(file_path: str) -> int:
localServer.newConnection.connect(
lambda: on_new_connection(localServer=localServer)
)
+ env = patch_env()
+ context = AppContext(name="Metasweeper", version="1.0.0", display_name="元扫雷",
+ plugin_dir=(Path(get_paths()) /
+ "plugins").as_posix(),
+ app_dir=get_paths()
+ )
+ PluginManager.instance().context = context
+
+ PluginManager.instance().start(Path(get_paths()) / "plugins", env)
mainWindow = mainWindowGUI.MainWindow()
ui = mineSweeperGUI.MineSweeperGUI(mainWindow, sys.argv)
ui.mainWindow.show()
From 7e792b3b445c00d26a900078581db02bab146fee Mon Sep 17 00:00:00 2001
From: ljzloser <1312358581@qq.com>
Date: Fri, 26 Dec 2025 20:04:20 +0800
Subject: [PATCH 5/7] =?UTF-8?q?feat(history=5Fgui):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E8=BF=87=E6=BB=A4=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=E7=BB=84=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
新增逻辑符号和比较符号枚举类,实现历史记录表格的过滤功能,支持通过组合条件筛选历史记录数据
---
src/history_gui.py | 120 ++++++-
src/ui/de_DE.ts | 292 ++++++++++++------
src/ui/en_US.ts | 292 ++++++++++++------
src/ui/pl_PL.ts | 292 ++++++++++++------
...346\210\220ts\346\226\207\344\273\266.bat" | 6 +-
src/utils.py | 2 +-
6 files changed, 723 insertions(+), 281 deletions(-)
diff --git a/src/history_gui.py b/src/history_gui.py
index 7b54ce9..49692bb 100644
--- a/src/history_gui.py
+++ b/src/history_gui.py
@@ -9,12 +9,59 @@
from typing import Any
from PyQt5.QtGui import QCloseEvent
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QTableWidget, QMenu, \
- QAction, QTableWidgetItem, QHeaderView, QTableView, QMessageBox, QFileDialog
+ QAction, QTableWidgetItem, QHeaderView, QTableView, QMessageBox, QFileDialog, \
+ QComboBox, QLineEdit
from PyQt5.QtCore import QDateTime, Qt, QCoreApplication
from datetime import datetime
import inspect
from utils import GameBoardState, BaseDiaPlayEnum, get_paths, patch_env
_translate = QCoreApplication.translate
+# 逻辑符
+
+
+class LogicSymbol(BaseDiaPlayEnum):
+ And = 0
+ Or = 1
+
+ @property
+ def display_name(self):
+ match self:
+ case LogicSymbol.And:
+ return _translate("Form", "与")
+ case LogicSymbol.Or:
+ return _translate("Form", "或")
+# 比较符
+
+
+class CompareSymbol(BaseDiaPlayEnum):
+ Equal = 0
+ NotEqual = 1
+ GreaterThan = 2
+ LessThan = 3
+ GreaterThanOrEqual = 4
+ LessThanOrEqual = 5
+ Contains = 6
+ NotContains = 7
+
+ @property
+ def display_name(self):
+ match self:
+ case CompareSymbol.Equal:
+ return _translate("Form", "等于")
+ case CompareSymbol.NotEqual:
+ return _translate("Form", "不等于")
+ case CompareSymbol.GreaterThan:
+ return _translate("Form", "大于")
+ case CompareSymbol.LessThan:
+ return _translate("Form", "小于")
+ case CompareSymbol.GreaterThanOrEqual:
+ return _translate("Form", "大于等于")
+ case CompareSymbol.LessThanOrEqual:
+ return _translate("Form", "小于等于")
+ case CompareSymbol.Contains:
+ return _translate("Form", "包含")
+ case CompareSymbol.NotContains:
+ return _translate("Form", "不包含")
class HistoryData:
@@ -83,6 +130,75 @@ def from_dict(cls, data: dict):
return instance
+class FliterWidget(QWidget):
+ def __init__(self, parent: QWidget | None = ...) -> None:
+ super().__init__(parent)
+ self.vbox = QVBoxLayout(self)
+ self.table = QTableWidget(self)
+ self.table.setColumnCount(6)
+ self.table.setHorizontalHeaderLabels(
+ ["左括号", "字段", "比较符", "值", "右括号", "逻辑符"])
+ self.table.horizontalHeader().setDefaultAlignment(Qt.AlignCenter)
+ # 自定义右键菜单
+ self.table.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.table.customContextMenuRequested.connect(self.show_context_menu)
+ self.vbox.addWidget(self.table)
+ self.setLayout(self.vbox)
+ self.add_row()
+
+ def show_context_menu(self, pos):
+ menu = QMenu(self)
+ menu.addAction(_translate("Form", "添加"), self.add_row)
+ menu.addAction(_translate("Form", "删除"), self.del_row)
+ menu.addAction(_translate("Form", "插入"),
+ lambda: self.insert_row(self.table.currentRow()))
+ menu.exec_(self.table.mapToGlobal(pos))
+
+ def build_left_bracket_Widget(self):
+ widget = QComboBox(self)
+ widget.addItems(["", "(", "(("])
+ return widget
+
+ def build_field_Widget(self):
+ widget = QComboBox(self)
+ widget.addItems(HistoryData.fields())
+ return widget
+
+ def build_compare_Widget(self):
+ widget = QComboBox(self)
+ widget.addItems(CompareSymbol.display_names())
+ return widget
+
+ def build_value_Widget(self):
+ widget = QLineEdit(self)
+ return widget
+
+ def build_right_bracket_Widget(self):
+ widget = QComboBox(self)
+ widget.addItems(["", ")", "))"])
+ return widget
+
+ def build_logic_Widget(self):
+ widget = QComboBox(self)
+ widget.addItems(LogicSymbol.display_names())
+ return widget
+
+ def add_row(self):
+ self.insert_row(self.table.rowCount())
+
+ def del_row(self):
+ pass
+
+ def insert_row(self, row: int):
+ self.table.insertRow(row)
+ self.table.setCellWidget(row, 0, self.build_left_bracket_Widget())
+ self.table.setCellWidget(row, 1, self.build_field_Widget())
+ self.table.setCellWidget(row, 2, self.build_compare_Widget())
+ self.table.setCellWidget(row, 3, self.build_value_Widget())
+ self.table.setCellWidget(row, 4, self.build_right_bracket_Widget())
+ self.table.setCellWidget(row, 5, self.build_logic_Widget())
+
+
class HistoryTable(QWidget):
def __init__(self, showFields: set[str], parent: QWidget | None = ...) -> None:
super().__init__(parent)
@@ -264,6 +380,8 @@ def __init__(self, parent=None):
self.resize(800, 600)
self.layout = QVBoxLayout(self)
self.table = HistoryTable(self.get_show_fields(), self)
+ self.fliterWidget = FliterWidget(self)
+ self.layout.addWidget(self.fliterWidget)
self.layout.addWidget(self.table)
self.setLayout(self.layout)
self.load_data()
diff --git a/src/ui/de_DE.ts b/src/ui/de_DE.ts
index 5149f2c..126896e 100644
--- a/src/ui/de_DE.ts
+++ b/src/ui/de_DE.ts
@@ -4,184 +4,167 @@
Form
-
+
确定
OK
-
+
取消
Abbrechen
-
+
结束后标雷
Minen markieren nach Spielende
-
+
标识:
Markierungen:
-
+
永远使用筛选法埋雷(不推荐)
Immer Minen nach Zufallsprinzip platzieren (nicht empfohlen)
-
+
自动保存录像(推荐)
Automatisches Speichern von Videos (empfohlen)
-
+
游戏模式:
Spielmodus:
-
+
方格边长:
Quadratische Seitenlängen:
-
+
标准
Standard
-
+
Win7
Win7
-
+
强无猜
Stark & Ungeschnitten
-
+
弱无猜
Schwäche ohne zu raten
-
+
经典无猜
Klassisch - Kein Raten
-
+
准无猜
Nicht erraten
-
+
强可猜
Starke Vermutung
-
+
弱可猜
Schwach erratbar
-
+
尝试次数:
Anzahl der Versuche:
-
+
局面约束:
Situative Zwänge:
-
+
自动重开:
Automatische Wiedereröffnung:
-
+
国家或地区:
Land oder Region:
-
+
关于
Über
-
- 项目主页:
- Projekt-Homepage:
-
-
-
- 资料教程:
- Informations-Tutorials:
-
-
-
- ①本软件可以不受任何限制地复制、储存、传播。
-②任何人可以在任何一个项目中使用本项目源代码的任何一个部分,同时欢迎在本项目主页提出宝贵的意见。
- ①Diese Software kann ohne jegliche Einschränkungen kopiert, gespeichert und verteilt werden.
-②Jeder kann jeden Teil des Quellcodes dieses Projekts in jedem Projekt verwenden und ist auch willkommen, wertvolle Kommentare auf dieser Projekt-Homepage zu geben.
-
-
-
+
自定义设置
Benutzerdefinierte Einstellungen
-
+
行数(row)
Zeilen
-
+
列数(column)
Spalten
-
+
雷数(number)
Minenanzahl
-
+
模式:
Modus:
-
+
雷数:
Anzahl der Donner:
-
+
高度:
Höhe:
-
+
宽度:
Breite:
-
+
边长:
Seitenlänge:
-
+
快捷键[4]
Tastenkombinationen [4]
-
+
快捷键[5]
Tastenkombinationen [5]
-
+
快捷键[6]
Tastenkombinationen [6]
@@ -191,215 +174,340 @@
Zähler
-
+
恭喜打破:
Herzlichen Glückwunsch zu Break:
-
+
未标雷(标准)
Nicht markierte Minen (Standard)
-
+
Time成绩!
Time Errungenschaften!
-
+
3BV/s成绩!
3BV/s Errungenschaften!
-
+
STNB成绩!
STNB Errungenschaften!
-
+
IOE成绩!
IOE Errungenschaften!
-
+
Path成绩!
Path Errungenschaften!
-
+
RQP成绩!
RQP Errungenschaften!
-
+
初级
Junior
-
+
个人最佳!
Persönliche Bestleistung!
-
+
中级
Fortgeschrittene
-
+
高级
Experte
-
+
勾选后永远使用筛选法埋雷,否则会适时改用调整法
Wenn diese Option aktiviert ist, wird beim Verlegen von Minen immer die Screening-Methode verwendet. Andernfalls wird gegebenenfalls auf die Anpassungsmethode umgeschaltet
-
+
完成后自动将录像保存到replay文件夹下
Aufzeichnung nach Abschluss automatisch im Ordner "Replay" speichern
-
+
允许纪录弹窗(推荐)
Popup für Aufzeichnung zulassen (empfohlen)
-
+
用于参加比赛
Zur Turnierteilnahme
-
+
比赛标识:
Turnierkennung:
-
+
光标不能超出边框
Der Cursor darf nicht über den Rand hinausragen
-
+
用于与其他人相区分,但不希望排名网站和软件展示出来
Dient dazu, sich von anderen zu unterscheiden, ist jedoch nicht für die Anzeige durch Ranking-Websites und Software vorgesehen
-
+
个性标识:
Personalisierte Identifizierung:
-
+
重播
Wiederholung
-
+
播放/暂停
Abspielen/Pause
-
+
滑动滚轮修改播放速度
Scrollrad zum Ändern der Abspielgeschwindigkeit
-
+
自动保存录像集
"evfs" automatisch speichern
-
+
Return
Return
-
+
插件管理
-
+
插件详情
-
+
设置
Einstellungen
-
+
保存
Speichern
-
+
保存成功
-
+
设置已保存
-
+
进程ID
-
+
插件名称
-
+
插件显示名称
-
+
插件描述
-
+
插件版本
-
+
作者
-
+
作者邮箱
-
+
插件URL
-
+
插件状态
-
+
心跳时间
-
+
订阅事件
+
+
+ 元扫雷是由资深扫雷玩家与软件工程师共同打造的一款现代化复刻。
+
+
+
+
+ Copyright © 2020-2025 元扫雷开发团队, 版权所有
+
+
+
+
+ 教程
+
+
+
+
+ 开发:王嘉宁、李京志
+
+
+
+
+ 致谢:濮天羿、向飞宇、钟言、翁逸杰、张砷镓
+
+
+
+
+ 元扫雷接受有益的贡献,包括新的玩法、规则、插件等。
+
+
+
+
+ 反馈
+
+
+
+
+ 感谢您考虑支持我们的开源项目,赞助时请备注项目名称+您的称呼+其他要求,例如元扫雷+张先生+建议添加**功能。您的赞助将有助于项目的持续发展和改进,使我们能够继续提高软件的质量。
+
+
+
+
+ 赞助
+
+
+
+
+ 1. 在非商业用途前提下,用户有权不受任何限制地对“元扫雷”软件进行复制、存储及传播。
+
+
+
+
+ 2. 由“元扫雷”软件生成的录像文件,其全部所有权归对应玩家本人所有。
+
+
+
+
+ <html><head/><body><p>3. 本项目源代码遵循GPLv3并附加额外条款发布。该额外条款特别禁止任何未经开发团队授权的商业使用行为,并对项目相关收益的分配方式作出明确约定。具体内容详见<a href="https://github.com/eee555/Metasweeper/blob/master/LICENSE"><span style=" text-decoration: underline; color:#0000ff;">LICENSE</span></a></p></body></html>
+
+
+
+
+ 协议
+
+
+
+
+ 播放
+
+
+
+
+ 导出
+
+
+
+
+ 刷新
+
+
+
+
+ 显示字段
+
+
+
+
+ 导出evf文件
+
+
+
+
+ 历史记录
+
+
+
+
+ 胜利
+
+
+
+
+ 失败
+
+
+
+
+ 准备
+
+
+
+
+ 进行中
+
+
+
+
+ 预标记
+
+
+
+
+ 回放
+ Wiederholung
+
MainWindow
diff --git a/src/ui/en_US.ts b/src/ui/en_US.ts
index 973a9c2..d9d8c88 100644
--- a/src/ui/en_US.ts
+++ b/src/ui/en_US.ts
@@ -4,189 +4,172 @@
Form
-
+
确定
OK
-
+
取消
Cancel
-
+
结束后标雷
Fill Finished Board With Flags
-
+
标识:
User identifier:
-
+
永远使用筛选法埋雷(不推荐)
Use filtering algorithm (not recommended)
-
+
自动保存录像(推荐)
Auto save video (recommended)
-
+
游戏模式:
Mode:
-
+
方格边长:
Zoom:
-
+
标准
Standard
-
+
Win7
Win7
-
+
强无猜
Strict no guess
-
+
弱无猜
Weak no guess
-
+
经典无猜
Classic no guess
-
+
准无猜
Blessing mode
-
+
强可猜
Guessable no guess
-
+
弱可猜
Lucky mode
-
+
自动重开:
Auto restart:
-
+
允许纪录弹窗(推荐)
Allow popovers for best scores (recommended)
-
+
尝试次数:
Attempts:
-
+
局面约束:
Constraint:
-
+
关于
About
-
+
自定义设置
Custom Settings
-
+
模式:
Mode:
-
+
雷数:
Mines:
-
+
高度:
Height:
-
+
宽度:
Width:
-
+
边长:
Zoom:
-
+
快捷键[4]
Shortcut [4]
-
+
快捷键[5]
Shortcut [5]
-
+
快捷键[6]
Shortcut [6]
-
+
行数(row)
row
-
+
列数(column)
column
-
+
雷数(number)
mine number
-
- 项目主页:
- Home:
-
-
-
- 资料教程:
- Tutorials:
-
-
-
- ①本软件可以不受任何限制地复制、储存、传播。
-②任何人可以在任何一个项目中使用本项目源代码的任何一个部分,同时欢迎在本项目主页提出宝贵的意见。
- ①This software can be copied, stored and transmitted without any restrictions.
-②Anyone can use any part of the project source code in any project, and you are welcome to make valuable comments on the project home page.
-
-
-
+
国家或地区:
Country or region:
@@ -196,210 +179,335 @@
Counters
-
+
恭喜打破:
Congratulations:
-
+
初级
Beginner
-
+
个人最佳!
PB!
-
+
中级
Intermediate
-
+
高级
Expert
-
+
勾选后永远使用筛选法埋雷,否则会适时改用调整法
Tick the box to always use the filtering method to lay mines, otherwise the adjustment method will be used at the right time
-
+
完成后自动将录像保存到replay文件夹下
After completion, automatically save the recording to the replay folder
-
+
未标雷(标准)
NF(Standard)
-
+
Time成绩!
Time score!
-
+
3BV/s成绩!
3BV/s score!
-
+
STNB成绩!
STNB score!
-
+
IOE成绩!
IOE score!
-
+
Path成绩!
Path score!
-
+
RQP成绩!
RQP score!
-
+
用于参加比赛
The credential for participating in the competition
-
+
比赛标识:
Championship identifier:
-
+
光标不能超出边框
The cursor cannot move out of the border
-
+
用于与其他人相区分,但不希望排名网站和软件展示出来
Used to distinguish from others, but not intended to be displayed on ranking websites and software
-
+
个性标识:
Unique identifier:
-
+
重播
Replay
-
+
播放/暂停
Play/Pause
-
+
滑动滚轮修改播放速度
Scroll the mouse wheel to adjust the playing speed
-
+
自动保存录像集
Auto save evfs
-
+
Return
Return
-
+
插件管理
Plugin Management
-
+
插件详情
Plugin details
-
+
设置
Options
-
+
保存
Save
-
+
保存成功
Saved successfully
-
+
设置已保存
Settings saved
-
+
进程ID
PID
-
+
插件名称
Name
-
+
插件显示名称
Display Name
-
+
插件描述
Description
-
+
插件版本
Version
-
+
作者
Author
-
+
作者邮箱
Author Email
-
+
插件URL
URL
-
+
插件状态
Status
-
+
心跳时间
Heartbeat Time
-
+
订阅事件
Subscription Events
+
+
+ 元扫雷是由资深扫雷玩家与软件工程师共同打造的一款现代化复刻。
+
+
+
+
+ Copyright © 2020-2025 元扫雷开发团队, 版权所有
+
+
+
+
+ 教程
+
+
+
+
+ 开发:王嘉宁、李京志
+
+
+
+
+ 致谢:濮天羿、向飞宇、钟言、翁逸杰、张砷镓
+
+
+
+
+ 元扫雷接受有益的贡献,包括新的玩法、规则、插件等。
+
+
+
+
+ 反馈
+
+
+
+
+ 感谢您考虑支持我们的开源项目,赞助时请备注项目名称+您的称呼+其他要求,例如元扫雷+张先生+建议添加**功能。您的赞助将有助于项目的持续发展和改进,使我们能够继续提高软件的质量。
+
+
+
+
+ 赞助
+
+
+
+
+ 1. 在非商业用途前提下,用户有权不受任何限制地对“元扫雷”软件进行复制、存储及传播。
+
+
+
+
+ 2. 由“元扫雷”软件生成的录像文件,其全部所有权归对应玩家本人所有。
+
+
+
+
+ <html><head/><body><p>3. 本项目源代码遵循GPLv3并附加额外条款发布。该额外条款特别禁止任何未经开发团队授权的商业使用行为,并对项目相关收益的分配方式作出明确约定。具体内容详见<a href="https://github.com/eee555/Metasweeper/blob/master/LICENSE"><span style=" text-decoration: underline; color:#0000ff;">LICENSE</span></a></p></body></html>
+
+
+
+
+ 协议
+
+
+
+
+ 播放
+ Play
+
+
+
+ 导出
+ Export
+
+
+
+ 刷新
+ Refresh
+
+
+
+ 显示字段
+ ShowField
+
+
+
+ 导出evf文件
+ Export To EVF
+
+
+
+ 历史记录
+ History
+
+
+
+ 胜利
+ Win
+
+
+
+ 失败
+ Loss
+
+
+
+ 准备
+ Ready
+
+
+
+ 进行中
+ Playing
+
+
+
+ 预标记
+ PreFlaging
+
+
+
+ 回放
+ Display
+
MainWindow
diff --git a/src/ui/pl_PL.ts b/src/ui/pl_PL.ts
index bee306a..8934b2e 100644
--- a/src/ui/pl_PL.ts
+++ b/src/ui/pl_PL.ts
@@ -4,184 +4,167 @@
Form
-
+
确定
Czy na pewno
-
+
取消
Anuluj
-
+
结束后标雷
Po zakończeniu znaku
-
+
标识:
Logotyp:
-
+
永远使用筛选法埋雷(不推荐)
Zawsze używaj metody przesiewowej do zakopywania min (niezalecane)
-
+
自动保存录像(推荐)
Automatyczne zapisywanie nagrań (zalecane)
-
+
游戏模式:
Tryb gry:
-
+
方格边长:
Długość boku siatki:
-
+
标准
norma
-
+
Win7
Win7
-
+
强无猜
Silny, bez zgadywania
-
+
弱无猜
Słaby bez zgadywania
-
+
经典无猜
Klasyczna niewinność
-
+
准无猜
Quasi-zgadywanie
-
+
强可猜
Mocne odgadnięcie
-
+
弱可猜
Słabe do odgadnięcia
-
+
尝试次数:
Liczba prób:
-
+
局面约束:
Ograniczenia sytuacyjne:
-
+
自动重开:
Automatyczne ponowne otwarcie:
-
+
国家或地区:
Kraj lub region:
-
+
关于
o
-
- 项目主页:
- Strona główna projektu:
-
-
-
- 资料教程:
- Samouczek informacyjny:
-
-
-
- ①本软件可以不受任何限制地复制、储存、传播。
-②任何人可以在任何一个项目中使用本项目源代码的任何一个部分,同时欢迎在本项目主页提出宝贵的意见。
- (1) Niniejsze oprogramowanie może być kopiowane, przechowywane i rozpowszechniane bez żadnych ograniczeń.
-(2) Każdy może użyć dowolnej części kodu źródłowego tego projektu w dowolnym projekcie i mile widziane cenne komentarze na stronie głównej tego projektu.
-
-
-
+
自定义设置
Dostosowywanie ustawień
-
+
行数(row)
Liczba wierszy
-
+
列数(column)
Liczba kolumn
-
+
雷数(number)
Liczba grzmotów
-
+
模式:
Tryb:
-
+
雷数:
Liczba grzmotów:
-
+
高度:
Wysokość:
-
+
宽度:
Szerokość:
-
+
边长:
Długość boku:
-
+
快捷键[4]
Skróty[4]
-
+
快捷键[5]
Skróty[5]
-
+
快捷键[6]
Skróty[6]
@@ -191,215 +174,340 @@
lada
-
+
恭喜打破:
Gratulujemy złamania:
-
+
未标雷(标准)
Nieoznakowana kopalnia (standard)
-
+
Time成绩!
Time czasowy!
-
+
3BV/s成绩!
3BV/s czasowy!
-
+
STNB成绩!
STNB czasowy!
-
+
IOE成绩!
IOE czasowy!
-
+
Path成绩!
Path czasowy!
-
+
RQP成绩!
RQP czasowy!
-
+
初级
młodszy
-
+
个人最佳!
Rekord życiowy!
-
+
中级
pośredni
-
+
高级
Starszy
-
+
勾选后永远使用筛选法埋雷,否则会适时改用调整法
Po zaznaczeniu tej opcji należy zawsze stosować metodę przesiewania do zakopywania min; w przeciwnym razie należy odpowiednio przełączyć się na metodę regulacji
-
+
完成后自动将录像保存到replay文件夹下
Po zakończeniu nagranie zostanie automatycznie zapisane w folderze „replay”
-
+
允许纪录弹窗(推荐)
Zezwól na powiadomienia wyskakujące (zalecane)
-
+
用于参加比赛
Do użytku podczas zawodów
-
+
比赛标识:
Logo konkursu:
-
+
光标不能超出边框
Kursor nie może wychodzić poza granice
-
+
用于与其他人相区分,但不希望排名网站和软件展示出来
Służy do odróżnienia się od innych, ale nie jest przeznaczony do wyświetlania przez strony internetowe i oprogramowanie zajmujące się rankingami
-
+
个性标识:
Identyfikacja spersonalizowana:
-
+
重播
Powtórka
-
+
播放/暂停
Odtwórz/Wstrzymaj
-
+
滑动滚轮修改播放速度
Reguluj prędkość odtwarzania, przesuwając pokrętło
-
+
自动保存录像集
Automatycznie zapisywana kolekcja filmów
-
+
Return
Return
-
+
插件管理
-
+
插件详情
-
+
设置
Zakładać
-
+
保存
Zapisz
-
+
保存成功
-
+
设置已保存
-
+
进程ID
-
+
插件名称
-
+
插件显示名称
-
+
插件描述
-
+
插件版本
-
+
作者
-
+
作者邮箱
-
+
插件URL
-
+
插件状态
-
+
心跳时间
-
+
订阅事件
+
+
+ 元扫雷是由资深扫雷玩家与软件工程师共同打造的一款现代化复刻。
+
+
+
+
+ Copyright © 2020-2025 元扫雷开发团队, 版权所有
+
+
+
+
+ 教程
+
+
+
+
+ 开发:王嘉宁、李京志
+
+
+
+
+ 致谢:濮天羿、向飞宇、钟言、翁逸杰、张砷镓
+
+
+
+
+ 元扫雷接受有益的贡献,包括新的玩法、规则、插件等。
+
+
+
+
+ 反馈
+
+
+
+
+ 感谢您考虑支持我们的开源项目,赞助时请备注项目名称+您的称呼+其他要求,例如元扫雷+张先生+建议添加**功能。您的赞助将有助于项目的持续发展和改进,使我们能够继续提高软件的质量。
+
+
+
+
+ 赞助
+
+
+
+
+ 1. 在非商业用途前提下,用户有权不受任何限制地对“元扫雷”软件进行复制、存储及传播。
+
+
+
+
+ 2. 由“元扫雷”软件生成的录像文件,其全部所有权归对应玩家本人所有。
+
+
+
+
+ <html><head/><body><p>3. 本项目源代码遵循GPLv3并附加额外条款发布。该额外条款特别禁止任何未经开发团队授权的商业使用行为,并对项目相关收益的分配方式作出明确约定。具体内容详见<a href="https://github.com/eee555/Metasweeper/blob/master/LICENSE"><span style=" text-decoration: underline; color:#0000ff;">LICENSE</span></a></p></body></html>
+
+
+
+
+ 协议
+
+
+
+
+ 播放
+
+
+
+
+ 导出
+
+
+
+
+ 刷新
+
+
+
+
+ 显示字段
+
+
+
+
+ 导出evf文件
+
+
+
+
+ 历史记录
+
+
+
+
+ 胜利
+
+
+
+
+ 失败
+
+
+
+
+ 准备
+
+
+
+
+ 进行中
+
+
+
+
+ 预标记
+
+
+
+
+ 回放
+ Powtórka
+
MainWindow
diff --git "a/src/ui/\347\224\237\346\210\220ts\346\226\207\344\273\266.bat" "b/src/ui/\347\224\237\346\210\220ts\346\226\207\344\273\266.bat"
index 8726d36..eda0376 100644
--- "a/src/ui/\347\224\237\346\210\220ts\346\226\207\344\273\266.bat"
+++ "b/src/ui/\347\224\237\346\210\220ts\346\226\207\344\273\266.bat"
@@ -1,5 +1,5 @@
-pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py -ts en_US.ts -noobsolete
+pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py ../history_gui.py ../utils.py -ts en_US.ts -noobsolete
-pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py -ts pl_PL.ts -noobsolete
+pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py ../history_gui.py ../utils.py -ts pl_PL.ts -noobsolete
-pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py -ts de_DE.ts -noobsolete
+pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py ../history_gui.py ../utils.py -ts de_DE.ts -noobsolete
diff --git a/src/utils.py b/src/utils.py
index c560e6c..3fe13dc 100644
--- a/src/utils.py
+++ b/src/utils.py
@@ -63,7 +63,7 @@ def display_name(self):
case GameBoardState.PreFlaging:
return _translate("Form", "预标记")
case GameBoardState.Display:
- return _translate("Form", "显示")
+ return _translate("Form", "回放")
def get_paths():
From 7c7883535317443bb6c456ccf6c52539e75758ba Mon Sep 17 00:00:00 2001
From: ljzloser <1312358581@qq.com>
Date: Fri, 26 Dec 2025 20:11:01 +0800
Subject: [PATCH 6/7] =?UTF-8?q?style(utils):=20=E6=B8=85=E7=90=86=E4=BB=A3?=
=?UTF-8?q?=E7=A0=81=E6=A0=BC=E5=BC=8F=E5=92=8C=E5=86=97=E4=BD=99=E4=BB=A3?=
=?UTF-8?q?=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
删除重复代码行,调整代码缩进和空格格式,移除无用空行和注释
---
src/utils.py | 57 +++++++++++++++-------------------------------------
1 file changed, 16 insertions(+), 41 deletions(-)
diff --git a/src/utils.py b/src/utils.py
index 725764f..decf8c2 100644
--- a/src/utils.py
+++ b/src/utils.py
@@ -315,7 +315,6 @@ def board(pp):
# poses是将要打开的多个位置,尝试调整board,使得game_board不变,而这些位置不再是雷。
# poses中至少有一个踩雷了。poses必须由同一个操作引起,例如单次双击
# 返回修改后的board和成功标识位
-def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]],
def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]],
game_board: List[List[int]],
poses: List[Tuple[int, int]]) -> Tuple[List[List[int]], bool]:
@@ -352,13 +351,11 @@ def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]],
if game_board[i][j] == 11:
game_board[i][j] = 10
game_board = ms.mark_board(game_board, remark=True)
- if any([game_board[x][y] == 11 for x, y in poses]):
if any([game_board[x][y] == 11 for x, y in poses]):
# 有一个必然是雷,就直接返回
return board, False
# 删去12不用管
- poses=list(filter(lambda xy: game_board[xy[0]][xy[1]] == 10, poses))
-
+ poses = list(filter(lambda xy: game_board[xy[0]][xy[1]] == 10, poses))
# 第一步,将board上的10分成三份,不变区0、无约束区1、数字约束区2
# 引入定理一:假如poses数量多于1,则必然全都在数字约束区
@@ -366,30 +363,27 @@ def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]],
# 统计这些区域的位置、雷数、非雷数
# 标记一下游戏局面。由于(mine_x, mine_y)是不确定的雷,因此标记过后一定还是10
# 记录每个格子的类型
- type_board=[[1 for i in range(len(board[0]))] for j in range(len(board))]
- rand_mine_num=0
- rand_blank_num=0
- constraint_mine_num=0
- constraint_blank_num=0
-
+ type_board = [[1 for i in range(len(board[0]))] for j in range(len(board))]
+ rand_mine_num = 0
+ rand_blank_num = 0
+ constraint_mine_num = 0
+ constraint_blank_num = 0
- matrix_ases, matrix_xses, matrix_bses=ms.refresh_matrixses(game_board)
+ matrix_ases, matrix_xses, matrix_bses = ms.refresh_matrixses(game_board)
for idb, block in enumerate(matrix_xses):
for idl, line in enumerate(block):
if poses[0] in line:
- if len(line) >= EnuLimit: # 枚举法的极限
- # 超过枚举极限时,暂时不能给出可能的解,有待升级
if len(line) >= EnuLimit: # 枚举法的极限
# 超过枚举极限时,暂时不能给出可能的解,有待升级
return board, False
- matrix_a=matrix_ases[idb][idl]
- matrix_x=matrix_xses[idb][idl]
- matrix_b=matrix_bses[idb][idl]
- constraint_mine_num=[board[x][y]
+ matrix_a = matrix_ases[idb][idl]
+ matrix_x = matrix_xses[idb][idl]
+ matrix_b = matrix_bses[idb][idl]
+ constraint_mine_num = [board[x][y]
for x, y in matrix_x].count(-1)
- constraint_mine_num=[board[x][y]
+ constraint_mine_num = [board[x][y]
for x, y in matrix_x].count(-1)
- constraint_blank_num=len(matrix_x) - constraint_mine_num
+ constraint_blank_num = len(matrix_x) - constraint_mine_num
for (i, j) in line:
type_board[i][j] = 2
else:
@@ -542,39 +536,31 @@ def trans_game_mode(mode: int) -> str:
# ...
-
-
class CoreBaseVideo(ms.BaseVideo):
mouse_state = 1
game_board_state = 1
x_y = (0, 0)
-
def __new__(cls, board, cell_pixel_size):
return ms.BaseVideo.__new__(cls, board, cell_pixel_size)
-
def __init__(self, board, cell_pixel_size):
super(CoreBaseVideo, self).__init__()
-
- @ property
+ @property
def game_board(self):
return self._game_board
-
- @ game_board.setter
+ @game_board.setter
def game_board(self, game_board):
self._game_board = game_board
- class AlwaysZero:
- def __getitem__(self, key):
-
class AlwaysZero:
def __getitem__(self, key):
class Inner:
def __getitem__(self, inner_key):
return 0
+
def __getitem__(self, inner_key):
return 0
return Inner()
@@ -584,7 +570,6 @@ def __getitem__(self, inner_key):
game_board_poss = AlwaysZero()
-
# unsolvableStructure = ms_toollib.py_unsolvableStructure
# unsolvableStructure2(BoardCheck)
# 用几种模板,检测局面中是否有明显的死猜的结构
@@ -592,7 +577,6 @@ def __getitem__(self, inner_key):
# 局面至少大于4*4
# 返回0或1
-def print2(arr, mode=0):
def print2(arr, mode=0):
# 调试时便于打印 print2(BoardofGame)
if mode == 0:
@@ -615,7 +599,6 @@ def print2(arr, mode=0):
print()
-
def debug_ms_board(ms_board):
for i in range(ms_board.events_len):
print(f"{ms_board.events_time(i)}: '{ms_board.events_mouse(i)}', ({ms_board.events_y(i)}, {ms_board.events_x(i)})")
@@ -683,11 +666,9 @@ def main():
# mine.compute_score(x, y)
# print(mine.mic())
-
# a = laymine_solvable(0, 10, 100000, (8, 8, 10, 0, 0, 100000))
# print(a)
-
# game_board = [[ 0, 1,10,10,10,10, 1, 0],
# [ 1, 3,10,10,10,10, 3, 1],
# [ 1,10,10,10,10,10,10, 1],
@@ -707,7 +688,6 @@ def main():
# [ 0, 1,-1,-1, 3,-1, 1, 0],
# ]
-
# game_board = [[10,10,10,10,10,10,10,10],
# [10,10,10,10,10,10,10,10],
# [10,10,10,10,10,10,10,10],
@@ -727,10 +707,8 @@ def main():
# [ 0, 0, 0, 0, 0, 0, 0, 0],
# ]
-
# print2(enumerateChangeBoard2(board, game_board, [(2, 3), (3, 2), (2, 2)])[0])
-
constraints = {}
board_constraint = "all([1,2,3])"
board_constraint = "all([1,2,3])"
@@ -742,12 +720,9 @@ def main():
except:
print("wrong")
-
...
-
if __name__ == '__main__':
-
main()
From 56ed2ff2d1fa7d27a4a71f07a58cf61750092d25 Mon Sep 17 00:00:00 2001
From: ljzloser <1312358581@qq.com>
Date: Fri, 26 Dec 2025 23:01:50 +0800
Subject: [PATCH 7/7] =?UTF-8?q?feat(history=5Fgui):=20=E9=87=8D=E6=9E=84?=
=?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E7=95=8C=E9=9D=A2=EF=BC=8C?=
=?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=88=86=E9=A1=B5=E5=92=8C=E9=AB=98=E7=BA=A7?=
=?UTF-8?q?=E6=9F=A5=E8=AF=A2=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 使用QAbstractTableModel重构历史记录表格,提升性能
- 增加高级查询功能,支持多种条件组合和括号优先级
- 实现分页功能,支持自定义每页显示数量
- 新增GameLevel枚举类型,完善游戏难度显示
- 优化字段类型处理,根据字段类型自动生成合适的输入控件
---
src/history_gui.py | 436 ++++++++++++++++++++++++++++-----
src/mp_plugins/events.py | 1 +
src/plugins/History/History.py | 3 +
src/utils.py | 61 +++--
4 files changed, 425 insertions(+), 76 deletions(-)
diff --git a/src/history_gui.py b/src/history_gui.py
index 49692bb..11f3d25 100644
--- a/src/history_gui.py
+++ b/src/history_gui.py
@@ -1,20 +1,20 @@
-from enum import Enum
-from fileinput import filename
+import math
import json
-import os
from pathlib import Path
import sqlite3
import subprocess
import sys
+from turtle import right
from typing import Any
from PyQt5.QtGui import QCloseEvent
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QTableWidget, QMenu, \
QAction, QTableWidgetItem, QHeaderView, QTableView, QMessageBox, QFileDialog, \
- QComboBox, QLineEdit
-from PyQt5.QtCore import QDateTime, Qt, QCoreApplication
+ QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox, QDateTimeEdit, QHBoxLayout, QPushButton, \
+ QSpacerItem, QSizePolicy, QLabel
+from PyQt5.QtCore import Qt, QCoreApplication, QAbstractTableModel, QModelIndex
from datetime import datetime
import inspect
-from utils import GameBoardState, BaseDiaPlayEnum, get_paths, patch_env
+from utils import GameBoardState, BaseDiaPlayEnum, get_paths, patch_env, GameMode, GameLevel
_translate = QCoreApplication.translate
# 逻辑符
@@ -30,7 +30,14 @@ def display_name(self):
return _translate("Form", "与")
case LogicSymbol.Or:
return _translate("Form", "或")
-# 比较符
+
+ @property
+ def to_sql(self):
+ match self:
+ case LogicSymbol.And:
+ return "and"
+ case LogicSymbol.Or:
+ return "or"
class CompareSymbol(BaseDiaPlayEnum):
@@ -63,6 +70,26 @@ def display_name(self):
case CompareSymbol.NotContains:
return _translate("Form", "不包含")
+ @property
+ def to_sql(self):
+ match self:
+ case CompareSymbol.Equal:
+ return "="
+ case CompareSymbol.NotEqual:
+ return "!="
+ case CompareSymbol.GreaterThan:
+ return ">"
+ case CompareSymbol.LessThan:
+ return "<"
+ case CompareSymbol.GreaterThanOrEqual:
+ return ">="
+ case CompareSymbol.LessThanOrEqual:
+ return "<="
+ case CompareSymbol.Contains:
+ return "in"
+ case CompareSymbol.NotContains:
+ return "not in"
+
class HistoryData:
replay_id: int = 0
@@ -74,7 +101,7 @@ class HistoryData:
left_s: float = 0.0
right_s: float = 0.0
double_s: float = 0.0
- level: int = 0
+ level: GameLevel = GameLevel.BEGINNER
cl: int = 0
cl_s: float = 0.0
ce: int = 0
@@ -90,7 +117,7 @@ class HistoryData:
etime: float = datetime.now()
start_time: datetime = datetime.now()
end_time: datetime = datetime.now()
- mode: int = 0
+ mode: GameMode = GameMode.Standard
software: str = ""
player_identifier: str = ""
race_identifier: str = ""
@@ -103,6 +130,13 @@ class HistoryData:
is_fair: int = 0
op: int = 0
isl: int = 0
+ pluck: float = 0.0
+
+ @classmethod
+ def get_field_value(cls, field_name: str):
+ for name, value in inspect.getmembers(cls):
+ if not name.startswith("__") and not callable(value) and not name.startswith("_") and name == field_name:
+ return value
@classmethod
def fields(cls):
@@ -130,6 +164,71 @@ def from_dict(cls, data: dict):
return instance
+class HistoryTableModel(QAbstractTableModel):
+ def __init__(self, data: list[HistoryData], headers: list[str], show_fields: set[str], parent=None):
+ super().__init__(parent)
+ self._data = data
+ self._headers = headers
+ self._show_fields = show_fields
+ # 只显示在show_fields中的列
+ self._visible_headers = [h for h in headers if h in show_fields]
+
+ def rowCount(self, parent=None):
+ return len(self._data)
+
+ def columnCount(self, parent=None):
+ return len(self._visible_headers)
+
+ def data(self, index: QModelIndex, role=Qt.DisplayRole):
+ if not index.isValid():
+ return None
+
+ row = index.row()
+ col = index.column()
+
+ if row >= len(self._data) or col >= len(self._visible_headers):
+ return None
+
+ if role == Qt.DisplayRole:
+ field_name = self._visible_headers[col]
+ value = getattr(self._data[row], field_name)
+
+ # 格式化显示值
+ if isinstance(value, datetime):
+ return value.strftime("%Y-%m-%d %H:%M:%S.%f")
+ elif isinstance(value, BaseDiaPlayEnum):
+ return value.display_name
+ else:
+ return str(value)
+
+ elif role == Qt.UserRole:
+ # 返回原始值
+ field_name = self._visible_headers[col]
+ return getattr(self._data[row], field_name)
+
+ elif role == Qt.TextAlignmentRole:
+ return Qt.AlignCenter | Qt.AlignVCenter
+
+ return None
+
+ def headerData(self, section: int, orientation: Qt.Orientation, role=Qt.DisplayRole):
+ if role == Qt.DisplayRole and orientation == Qt.Horizontal:
+ if section < len(self._visible_headers):
+ return self._visible_headers[section]
+ return None
+
+ def update_data(self, data: list[HistoryData]):
+ self.beginResetModel()
+ self._data = data
+ self.endResetModel()
+
+ def update_show_fields(self, show_fields: set[str]):
+ self.beginResetModel()
+ self._show_fields = show_fields
+ self._visible_headers = [h for h in self._headers if h in show_fields]
+ self.endResetModel()
+
+
class FliterWidget(QWidget):
def __init__(self, parent: QWidget | None = ...) -> None:
super().__init__(parent)
@@ -142,9 +241,10 @@ def __init__(self, parent: QWidget | None = ...) -> None:
# 自定义右键菜单
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.customContextMenuRequested.connect(self.show_context_menu)
+ self.table.setSelectionBehavior(QTableView.SelectRows)
+ self.table.setSelectionMode(QTableView.SingleSelection)
self.vbox.addWidget(self.table)
self.setLayout(self.vbox)
- self.add_row()
def show_context_menu(self, pos):
menu = QMenu(self)
@@ -162,15 +262,13 @@ def build_left_bracket_Widget(self):
def build_field_Widget(self):
widget = QComboBox(self)
widget.addItems(HistoryData.fields())
+ widget.currentIndexChanged.connect(self.on_field_changed)
return widget
def build_compare_Widget(self):
widget = QComboBox(self)
widget.addItems(CompareSymbol.display_names())
- return widget
-
- def build_value_Widget(self):
- widget = QLineEdit(self)
+ widget.currentIndexChanged.connect(self.on_compare_changed)
return widget
def build_right_bracket_Widget(self):
@@ -181,33 +279,175 @@ def build_right_bracket_Widget(self):
def build_logic_Widget(self):
widget = QComboBox(self)
widget.addItems(LogicSymbol.display_names())
+
+ return widget
+
+ def on_field_changed(self, index):
+ combo: QComboBox = self.sender()
+ item_index = self.table.indexAt(combo.pos())
+ filed_name = combo.currentText()
+ if item_index.isValid():
+ row = item_index.row()
+ compareSymbol_widget = self.table.cellWidget(row, 2)
+ compare_name = compareSymbol_widget.currentText()
+ compare = CompareSymbol.from_display_name(compare_name)
+ field_cls = HistoryData.get_field_value(filed_name)
+ widget = self.build_value_widget(compare, field_cls)
+ self.table.setCellWidget(row, 3, widget)
+
+ def on_compare_changed(self, index):
+ combo: QComboBox = self.sender()
+ item_index = self.table.indexAt(combo.pos())
+ if item_index.isValid():
+ row = item_index.row()
+ field_widget = self.table.cellWidget(row, 1)
+ filed_name = field_widget.currentText()
+ compare_name = combo.currentText()
+ compare = CompareSymbol.from_display_name(compare_name)
+ field_cls = HistoryData.get_field_value(filed_name)
+ widget = self.build_value_widget(compare, field_cls)
+ self.table.setCellWidget(row, 3, widget)
+
+ def build_value_widget(self, compare: CompareSymbol, field_value: Any):
+ if compare not in (CompareSymbol.Contains, CompareSymbol.NotContains):
+ if isinstance(field_value, int):
+ widget = QSpinBox(self)
+ elif isinstance(field_value, float):
+ widget = QDoubleSpinBox(self)
+ elif isinstance(field_value, str):
+ widget = QLineEdit(self)
+ elif isinstance(field_value, datetime):
+ widget = QDateTimeEdit(self)
+ elif isinstance(field_value, BaseDiaPlayEnum):
+ widget = QComboBox(self)
+ widget.addItems(field_value.display_names())
+ else:
+ widget = QLineEdit(self)
return widget
def add_row(self):
self.insert_row(self.table.rowCount())
def del_row(self):
- pass
+ self.table.removeRow(self.table.currentRow())
def insert_row(self, row: int):
self.table.insertRow(row)
+ field_widget = self.build_field_Widget()
+ compare_widget = self.build_compare_Widget()
+ compare = CompareSymbol.from_display_name(compare_widget.currentText())
+ field_value = HistoryData.get_field_value(field_widget.currentText())
self.table.setCellWidget(row, 0, self.build_left_bracket_Widget())
- self.table.setCellWidget(row, 1, self.build_field_Widget())
- self.table.setCellWidget(row, 2, self.build_compare_Widget())
- self.table.setCellWidget(row, 3, self.build_value_Widget())
+ self.table.setCellWidget(row, 1, field_widget)
+ self.table.setCellWidget(row, 2, compare_widget)
+ self.table.setCellWidget(
+ row, 3, self.build_value_widget(compare, field_value))
self.table.setCellWidget(row, 4, self.build_right_bracket_Widget())
self.table.setCellWidget(row, 5, self.build_logic_Widget())
+ def gen_fliter_str(self):
+ fliter_str = ""
+ left_count = 0
+ right_count = 0
+ for row in range(self.table.rowCount()):
+
+ left_bracket_widget = self.table.cellWidget(row, 0)
+ field_widget = self.table.cellWidget(row, 1)
+ compare_widget = self.table.cellWidget(row, 2)
+ value_widget = self.table.cellWidget(row, 3)
+ right_bracket_widget = self.table.cellWidget(row, 4)
+ logic_widget = self.table.cellWidget(row, 5)
+ left_bracket = left_bracket_widget.currentText()
+ field = field_widget.currentText()
+ field_init_value = HistoryData.get_field_value(field)
+ compare = CompareSymbol.from_display_name(
+ compare_widget.currentText())
+ right_bracket = right_bracket_widget.currentText()
+ logic = LogicSymbol.from_display_name(
+ logic_widget.currentText()).to_sql
+ if left_bracket == "(":
+ left_count += 1
+ elif left_bracket == "((":
+ left_count += 2
+
+ if right_bracket == ")":
+ right_count += 1
+ elif right_bracket == "))":
+ right_count += 2
+
+ if right_count > left_count:
+ QMessageBox.warning(self, "错误", f"第{row}行 右括号数量大于左括号数量,请检查")
+ return
+
+ if isinstance(value_widget, QComboBox):
+ filed_cls = type(field_init_value)
+ value = filed_cls.from_display_name(
+ value_widget.currentText()).value
+ elif isinstance(value_widget, QDateTimeEdit):
+ value = int(value_widget.dateTime(
+ ).toPyDateTime().timestamp() * 1_000_000)
+ elif isinstance(value_widget, QSpinBox):
+ value = str(value_widget.value())
+ elif isinstance(value_widget, QDoubleSpinBox):
+ value = str(value_widget.value())
+ elif isinstance(value_widget, QLineEdit):
+ if compare in (CompareSymbol.Contains, CompareSymbol.NotContains):
+ if isinstance(field_init_value, (int, float)):
+ values = value_widget.text().split(",")
+ for v in values:
+ if not v.isdigit():
+ QMessageBox.warning(
+ self, "错误", f"第{row}行 {v} 不是数字,请输入数字")
+ return None
+ value = ",".join(str(v) for v in values)
+ elif isinstance(field_init_value, BaseDiaPlayEnum):
+ values = value_widget.text().split(",")
+ filed_cls = type(field_init_value)
+ for v in values:
+ if v not in field_init_value.display_names():
+ QMessageBox.warning(
+ self, "错误", f"第{row}行 {v} 不是合法的枚举值,请输入合法的枚举值")
+ return None
+ values = [filed_cls.from_display_name(
+ v).value for v in values]
+ value = ",".join(str(v) for v in values)
+ elif isinstance(field_init_value, datetime):
+ values = value_widget.text().split(",")
+ for v in values:
+ try:
+ d = datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
+ except ValueError:
+ QMessageBox.warning(
+ self, "错误", f"第{row}行 {v} 不是合法的日期时间戳,请输入合法的日期时间戳,格式为: %Y-%m-%d %H:%M:%S")
+ return None
+ values = [int(datetime.strptime(
+ v, "%Y-%m-%d %H:%M:%S").timestamp() * 1_000_000) for d in values]
+ value = ",".join(str(v) for v in values)
+ else:
+ value = ",".join(
+ f"'{v}'" for v in value_widget.text().split(","))
+ value = f"({value})"
+ else:
+ value = f"'{value_widget.text()}'"
+ if row == self.table.rowCount() - 1:
+ fliter_str += f" {left_bracket} {field} {compare.to_sql} {value} {right_bracket} "
+ else:
+ fliter_str += f" {left_bracket} {field} {compare.to_sql} {value} {right_bracket} {logic}"
+ if left_count != right_count:
+ QMessageBox.warning(self, "错误", f"左括号数量和右括号数量不匹配,请检查")
+ return None
+ return fliter_str
+
class HistoryTable(QWidget):
def __init__(self, showFields: set[str], parent: QWidget | None = ...) -> None:
super().__init__(parent)
self.layout: QVBoxLayout = QVBoxLayout(self)
- self.table = QTableWidget(self)
+ self.table = QTableView(self)
self.layout.addWidget(self.table)
self.setLayout(self.layout)
# 设置不可编辑
- self.table.setEditTriggers(QTableWidget.NoEditTriggers)
+ self.table.setEditTriggers(QTableView.NoEditTriggers)
# 添加右键菜单
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.customContextMenuRequested.connect(self.show_context_menu)
@@ -251,40 +491,24 @@ def __init__(self, showFields: set[str], parent: QWidget | None = ...) -> None:
"is_fair",
"op",
"isl",
+ "pluck"
]
- self.table.setColumnCount(len(self.showFields))
- self.table.setHorizontalHeaderLabels(self.headers)
+
+ # 创建模型
+ self.model = HistoryTableModel([], self.headers, self.showFields, self)
+ self.table.setModel(self.model)
+
# 居中显示文字
self.table.horizontalHeader().setDefaultAlignment(Qt.AlignCenter)
# 选中整行
self.table.setSelectionBehavior(QTableView.SelectRows)
-
# 自适应列宽
self.table.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeToContents)
- # 初始化隐藏列
- for i, field in enumerate(self.headers):
- self.table.setColumnHidden(i, field not in self.showFields)
def load(self, data: list[HistoryData]):
- self.table.setRowCount(len(data))
- for i, row in enumerate(data):
- for j, field in enumerate(self.headers):
- value = getattr(row, field)
-
- self.table.setItem(i, j, self.build_item(value))
-
- def build_item(self, value: Any):
- if isinstance(value, datetime):
- new_value = value.strftime("%Y-%m-%d %H:%M:%S.%f")
- if isinstance(value, BaseDiaPlayEnum):
- new_value = value.display_name
- else:
- new_value = value
- item = QTableWidgetItem(str(new_value))
- item.setData(Qt.UserRole, value)
- item.setTextAlignment(Qt.AlignCenter | Qt.AlignVCenter)
- return item
+ # 使用模型更新数据
+ self.model.update_data(data)
def refresh(self):
parent: 'HistoryGUI' = self.parent()
@@ -311,23 +535,28 @@ def show_context_menu(self, pos):
def on_action_triggered(self, checked: bool):
action: QAction = self.sender()
name = action.text()
- self.table.setColumnHidden(
- self.table.horizontalHeader().logicalIndex(
- self.headers.index(name)), not checked)
if checked:
self.showFields.add(name)
else:
self.showFields.remove(name)
+ # 更新模型的显示字段
+ self.model.update_show_fields(self.showFields)
+
def save_evf(self, evf_path: str):
- row_index = self.table.currentRow()
+ row_index = self.table.currentIndex().row()
if row_index < 0:
return
- row = self.table.item(row_index, 0).data(Qt.UserRole)
- for filed in self.headers:
- if filed == "replay_id":
- replay_id = self.table.item(
- row_index, self.headers.index(filed)).data(Qt.UserRole)
+
+ # 从模型获取数据
+ replay_id_index = self.model._visible_headers.index(
+ "replay_id") if "replay_id" in self.model._visible_headers else -1
+ if replay_id_index >= 0:
+ replay_id = self.model.data(self.model.index(
+ row_index, replay_id_index), Qt.UserRole)
+ else:
+ # 如果replay_id不在显示字段中,从原始数据获取
+ replay_id = getattr(self.model._data[row_index], "replay_id")
conn = sqlite3.connect(Path(get_paths()) / "history.db")
conn.row_factory = sqlite3.Row # 设置行工厂
cursor = conn.cursor()
@@ -379,20 +608,109 @@ def __init__(self, parent=None):
self.setWindowTitle(_translate("Form", "历史记录"))
self.resize(800, 600)
self.layout = QVBoxLayout(self)
+ self.button_layout = QHBoxLayout()
+ self.query_button = QPushButton(_translate("Form", "查询"))
+ self.button_layout.addWidget(self.query_button)
+ self.button_layout.addItem(QSpacerItem(
+ 10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum))
+
self.table = HistoryTable(self.get_show_fields(), self)
self.fliterWidget = FliterWidget(self)
+
+ self.limit_layout = QHBoxLayout()
+ self.previous_button = QPushButton(_translate("Form", "上一页"))
+ self.page_spin = QSpinBox()
+ self.page_spin.setMinimum(1)
+ self.page_spin.setValue(1)
+ self.next_button = QPushButton(_translate("Form", "下一页"))
+ self.one_page_combo = QComboBox()
+ self.one_page_combo.addItems(
+ ["10", "20", "50", "100", "200", "500", "1000"])
+ self.limit_label = QLabel("")
+ self.limit_layout.addItem(QSpacerItem(
+ 10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum))
+ self.limit_layout.addWidget(self.limit_label)
+ self.limit_layout.addWidget(self.previous_button)
+ self.limit_layout.addWidget(self.page_spin)
+ self.limit_layout.addWidget(self.next_button)
+ self.limit_layout.addWidget(self.one_page_combo)
+
+ self.layout.addLayout(self.button_layout)
self.layout.addWidget(self.fliterWidget)
self.layout.addWidget(self.table)
+ self.layout.addLayout(self.limit_layout)
self.setLayout(self.layout)
+
+ self.init_connect()
+ self.load_data()
+
+ def init_connect(self):
+ self.query_button.clicked.connect(self.on_query_button_clicked)
+ self.previous_button.clicked.connect(self.previous_page)
+ self.next_button.clicked.connect(self.next_page)
+ self.one_page_combo.currentTextChanged.connect(self.one_page_changed)
+ self.page_spin.valueChanged.connect(self.page_changed)
+
+ def on_query_button_clicked(self):
+ if self.page_spin.value() > 1:
+ self.page_spin.setValue(1)
+ else:
+ self.load_data()
+
+ def previous_page(self):
+ self.page_spin.setValue(self.page_spin.value() - 1)
+
+ def next_page(self):
+ self.page_spin.setValue(self.page_spin.value() + 1)
+
+ def one_page_changed(self, text):
+ self.limit_changed()
+
+ def page_changed(self, value):
+ self.limit_changed()
+
+ def limit_changed(self):
self.load_data()
+ def get_limit_str(self):
+ return f" limit {self.one_page_combo.currentText()} offset {(self.page_spin.value() - 1) * int(self.one_page_combo.currentText())}"
+
def load_data(self):
- conn = sqlite3.connect(Path(get_paths()) / "history.db")
- conn.row_factory = sqlite3.Row # 设置行工厂
- cursor = conn.cursor()
- cursor.execute(HistoryData.query_all())
- datas = cursor.fetchall()
- history_data = [HistoryData.from_dict(dict(data)) for data in datas]
+ # 判断是否存在历史记录数据库
+ if not (Path(get_paths()) / "history.db").exists():
+ QMessageBox.warning(self, "错误", "历史记录数据库不存在")
+ return
+ try:
+ conn = sqlite3.connect(Path(get_paths()) / "history.db")
+ conn.row_factory = sqlite3.Row # 设置行工厂
+ cursor = conn.cursor()
+ filter_str = self.fliterWidget.gen_fliter_str()
+ sql = f"select *,COUNT(*) OVER() AS total_count from history"
+ if filter_str:
+ sql += " where " + filter_str
+ elif filter_str is None:
+ return
+ sql += self.get_limit_str()
+ cursor.execute(sql)
+ datas = cursor.fetchall()
+
+ if not datas:
+ self.page_spin.setMaximum(1)
+ self.limit_label.setText(f'共0行,0页')
+ else:
+ self.page_spin.setMaximum(
+ math.ceil(datas[0]['total_count'] / int(self.one_page_combo.currentText())))
+ self.limit_label.setText(
+ f'共{datas[0]["total_count"]}行,{self.page_spin.maximum()}页')
+
+ history_data = [HistoryData.from_dict(
+ dict(data)) for data in datas]
+ conn.close()
+ except sqlite3.Error as e:
+ QMessageBox.warning(
+ self, "错误", f"加载历史记录数据失败: {e}")
+ return
+
self.table.load(history_data)
@property
diff --git a/src/mp_plugins/events.py b/src/mp_plugins/events.py
index 67758eb..52d8041 100644
--- a/src/mp_plugins/events.py
+++ b/src/mp_plugins/events.py
@@ -39,4 +39,5 @@ class GameEndEvent(BaseEvent):
is_fair: int = 0
op: int = 0
isl: int = 0
+ pluck: float = 0
raw_data: str = ''
diff --git a/src/plugins/History/History.py b/src/plugins/History/History.py
index ce6a364..2e35a9f 100644
--- a/src/plugins/History/History.py
+++ b/src/plugins/History/History.py
@@ -92,6 +92,7 @@ def initialize(self) -> None:
is_fair INTEGER,
op INTEGER,
isl INTEGER,
+ pluck REAL,
raw_data BLOB
);
"""
@@ -150,6 +151,7 @@ def on_game_end(self, event: GameEndEvent):
is_fair,
op,
isl,
+ pluck,
raw_data
)
values
@@ -191,6 +193,7 @@ def on_game_end(self, event: GameEndEvent):
:is_fair,
:op,
:isl,
+ :pluck,
:raw_data
)
""",
diff --git a/src/utils.py b/src/utils.py
index decf8c2..0fb3cf1 100644
--- a/src/utils.py
+++ b/src/utils.py
@@ -67,7 +67,10 @@ def display_name(self):
class MouseState(BaseDiaPlayEnum):
'''
- 关于鼠标状态的枚举体,这些魔数遵循ms_toollib标准
+ 关于鼠标状态的枚举体。游戏过程中,鼠标的动作会触发鼠标事件,并在evf录像中记录为
+ 诸如"mv", "lc", "lr", "rc", "rr", "mc", "mr", "pf", "cc", "l", "r", "m"
+ 动作导致鼠标转移至不同的状态,用于计算左键、右键、双击等次数,显示局面高亮等
+ 这些魔数遵循ms_toollib标准
'''
UpUp = 1
UpDown = 2
@@ -81,21 +84,21 @@ class MouseState(BaseDiaPlayEnum):
@property
def display_name(self):
match self:
- case GameBoardState.UpUp:
+ case MouseState.UpUp:
return _translate("Form", "双键抬起")
- case GameBoardState.UpDown:
+ case MouseState.UpDown:
return _translate("Form", "右键按下且标过雷")
- case GameBoardState.UpDownNotFlag:
+ case MouseState.UpDownNotFlag:
return _translate("Form", "右键按下且没有标过雷")
- case GameBoardState.DownUp:
+ case MouseState.DownUp:
return _translate("Form", "左键按下")
- case GameBoardState.Chording:
+ case MouseState.Chording:
return _translate("Form", "双键按下")
- case GameBoardState.ChordingNotFlag:
+ case MouseState.ChordingNotFlag:
return _translate("Form", "双键按下且先按下右键且没有标雷")
- case GameBoardState.DownUpAfterChording:
+ case MouseState.DownUpAfterChording:
return _translate("Form", "双击后先弹起右键左键还没有弹起")
- case GameBoardState.Undefined:
+ case MouseState.Undefined:
return _translate("Form", "未初始化")
@@ -117,24 +120,48 @@ class GameMode(BaseDiaPlayEnum):
@property
def display_name(self):
match self:
- case GameBoardState.Standard:
+ case GameMode.Standard:
return _translate("Form", "标准")
- case GameBoardState.Win7:
+ case GameMode.Win7:
return _translate("Form", "win7")
- case GameBoardState.ClassicNoGuess:
+ case GameMode.ClassicNoGuess:
return _translate("Form", "经典无猜")
- case GameBoardState.StrictNoGuess:
+ case GameMode.StrictNoGuess:
return _translate("Form", "强无猜")
- case GameBoardState.WeakNoGuess:
+ case GameMode.WeakNoGuess:
return _translate("Form", "弱无猜")
- case GameBoardState.BlessingMode:
+ case GameMode.BlessingMode:
return _translate("Form", "准无猜")
- case GameBoardState.GuessableNoGuess:
+ case GameMode.GuessableNoGuess:
return _translate("Form", "强可猜")
- case GameBoardState.LuckyMode:
+ case GameMode.LuckyMode:
return _translate("Form", "弱可猜")
+class GameLevel(BaseDiaPlayEnum):
+ '''
+ 关于游戏难度的枚举体,这些魔数遵循evf标准(ms_toollib也是遵循evf标准)
+ 参考:
+ https://github.com/eee555/ms-toollib/blob/main/evf%E6%A0%87%E5%87%86.md
+ '''
+ BEGINNER = 3
+ INTERMEDIATE = 4
+ EXPERT = 5
+ CUSTOM = 6
+
+ @property
+ def display_name(self):
+ match self:
+ case GameLevel.BEGINNER:
+ return _translate("Form", "初级")
+ case GameLevel.INTERMEDIATE:
+ return _translate("Form", "中级")
+ case GameLevel.EXPERT:
+ return _translate("Form", "高级")
+ case GameLevel.CUSTOM:
+ return _translate("Form", "自定义")
+
+
def get_paths():
if getattr(sys, "frozen", False):
# 打包成 exe